Public/Invoke-M365QuickAssess.ps1

###################################################################################################
# Function: Invoke-M365QuickAssess
# Module: M365-QuickAssess
# Author: Ryan Holderread - Rackspace Technology
# Repository: https://github.com/HennepinCrawler/M365-QuickAssess
# Version: 2.0
# Last Updated: 2026-03-25
#
# Description:
# Main entry point for the M365 Quick Assessment module.
# Orchestrates workload collection, authentication, and JSON output.
# Run via Launch-M365Assessment.ps1 for automatic dependency handling.
###################################################################################################

# Ensure only loading modules for M365-QuickAssess if it has already been installed
$env:PSModulePath = ( $env:PSModulePath -split ";" | Where-Object { $_ -notmatch "M365-QuickAssess" } ) -join ";"

function Invoke-M365QuickAssess
{
    [CmdletBinding()]
    param
    (
        [ValidateSet("All","Entra","Exchange","Device","SharePoint","OneDrive","Teams","Purview","Azure","Copilot","PowerPlatform")]
        [string[]]$Workloads = @("All"),

        [string]$OutputPath = "C:\ProgramData\Rackspace-Technology"
    )

    # -------------------------------------------------------------------
    # Version Check
    # -------------------------------------------------------------------
    try
    {
        $installedVersion = Get-InstalledModule -Name M365-QuickAssess -ErrorAction Stop
        $galleryVersion   = Find-Module -Name M365-QuickAssess -Repository PSGallery -ErrorAction Stop

        if ( [version]$galleryVersion.Version -gt [version]$installedVersion.Version )
        {
            Write-Host ""
            Write-Host " A new version of M365-QuickAssess is available!" -ForegroundColor Yellow
            Write-Host " Installed : $( $installedVersion.Version )" -ForegroundColor Yellow
            Write-Host " Available : $( $galleryVersion.Version )" -ForegroundColor Yellow
            Write-Host " Run: Install-Module M365-QuickAssess -Force -Scope CurrentUser" -ForegroundColor Yellow
            Write-Host ""
        }
    }
    catch
    {
        Write-Log "Version check failed: $( $_.Exception.Message )" "WARN"
    }

    # -------------------------------------------------------------------
    # Initialize Context
    # -------------------------------------------------------------------
    $script:Context = @{
        TenantId      = $null
        TenantPrefix  = $null
        GraphAccount  = $null
        RunStamp      = ( Get-Date ).ToString("yyyyMMdd-HHmmss-fff")
        OutputPath    = $OutputPath
        WorkloadLabel = $null
    }

    # -------------------------------------------------------------------
    # Workload Flags
    # -------------------------------------------------------------------
    $RunAll        = $Workloads -contains "All"
    $RunEntra      = $RunAll -or ( $Workloads -contains "Entra" )
    $RunDevice     = $RunAll -or ( $Workloads -contains "Device" )
    $RunExchange   = $RunAll -or ( $Workloads -contains "Exchange" )
    $RunSharePoint = $RunAll -or ( $Workloads -contains "SharePoint" )
    $RunOneDrive   = $RunAll -or ( $Workloads -contains "OneDrive" )
    $RunTeams      = $RunAll -or ( $Workloads -contains "Teams" )
    $RunPurview    = $RunAll -or ( $Workloads -contains "Purview" )
    $RunAzure      = $RunAll -or ( $Workloads -contains "Azure" )
    $RunCopilot    = $RunAll -or ( $Workloads -contains "Copilot" )

    $script:Context.WorkloadLabel = if ( $RunAll )
    {
        "Full"
    }
    else
    {
        ( $Workloads | Where-Object { $_ } | ForEach-Object { $_.ToLower() } | Sort-Object ) -join "+"
    }

    # -------------------------------------------------------------------
    # Disclaimer / Intro
    # -------------------------------------------------------------------
    Write-AssessmentBanner

    # -------------------------------------------------------------------
    # Output Directory
    # -------------------------------------------------------------------
    if ( -not ( Test-Path $OutputPath ) )
    {
        New-Item -ItemType Directory -Path $OutputPath -Force | Out-Null
    }

    # -------------------------------------------------------------------
    # Assessment Schema
    # -------------------------------------------------------------------
    $Assessment = New-AssessmentSchema

    # -------------------------------------------------------------------
    # Connect to Microsoft Graph (required for all workloads)
    # -------------------------------------------------------------------
    $graphConnected = $false

    try
    {
        Connect-GraphService
        $graphConnected = $true
    }
    catch
    {
        Write-Log "Fatal: Graph connection failed. Cannot continue." "ERROR"
    }

    if ( -not $graphConnected )
    {
        Write-Host ""
        Write-Host " Assessment could not start - Graph connection failed." -ForegroundColor Red
        Write-Host " Check the log file for details: $( $script:Context.OutputPath )" -ForegroundColor Yellow
        Write-Host ""
        return
    }

    # Log workloads after connection so tenant prefix is available
    Write-Log "Workloads: $( $Workloads -join ', ' )"

    # -------------------------------------------------------------------
    # Metadata (always runs - fatal if it fails, nothing else will work)
    # -------------------------------------------------------------------
    $metadataOk = $false

    try
    {
        Get-AssessmentMetadata -Assessment $Assessment
        $metadataOk = $true
    }
    catch
    {
        Write-Log "Fatal: Metadata collection failed. Cannot continue." "ERROR"
    }

    if ( -not $metadataOk )
    {
        Write-Host ""
        Write-Host " Assessment could not start - metadata collection failed." -ForegroundColor Red
        Write-Host " Check the log file for details: $( $script:Context.OutputPath )" -ForegroundColor Yellow
        Write-Host ""
        return
    }

    # -------------------------------------------------------------------
    # Power Platform License Check (always runs - gates PP collection)
    # -------------------------------------------------------------------
    Test-PowerPlatformLicense -Assessment $Assessment

    # -------------------------------------------------------------------
    # Entra
    # -------------------------------------------------------------------
    if ( $RunEntra )
    {
        Write-Log "--- Starting Entra workload ---"

        Invoke-WithErrorHandling "LicenseData"        { Get-LicenseData           -Assessment $Assessment }
        Invoke-WithErrorHandling "UserData"           { Get-UserData              -Assessment $Assessment }
        Invoke-WithErrorHandling "ConditionalAccess"  { Get-ConditionalAccessData -Assessment $Assessment }
        Invoke-WithErrorHandling "UserUsage"          { Get-UserUsage             -Assessment $Assessment }
    }

    # -------------------------------------------------------------------
    # Device Management
    # -------------------------------------------------------------------
    if ( $RunDevice )
    {
        Write-Log "--- Starting Device Management workload ---"
        Invoke-WithErrorHandling "DeviceManagement" { Get-DeviceManagementData -Assessment $Assessment }
    }

    # -------------------------------------------------------------------
    # Exchange
    # -------------------------------------------------------------------
    if ( $RunExchange )
    {
        Write-Log "--- Starting Exchange workload ---"

        $exchangeConnected = $false

        try
        {
            Connect-ExchangeService
            $exchangeConnected = $true
        }
        catch
        {
            Write-Log "Exchange connection failed - skipping Exchange workload" "WARN"
        }

        if ( $exchangeConnected )
        {
            Invoke-WithErrorHandling "ExchangeData"  { Get-ExchangeData  -Assessment $Assessment }
            Invoke-WithErrorHandling "ExchangeUsage" { Get-ExchangeUsage -Assessment $Assessment }
            Invoke-WithErrorHandling "DNSData" { Get-DNSData -Assessment $Assessment }
        }
    }

    # -------------------------------------------------------------------
    # SharePoint
    # -------------------------------------------------------------------
    if ( $RunSharePoint )
    {
        Write-Log "--- Starting SharePoint workload ---"
        Invoke-WithErrorHandling "SharePointData"  { Get-SharePointData  -Assessment $Assessment }
        Invoke-WithErrorHandling "SharePointUsage" { Get-SharePointUsage -Assessment $Assessment }
    }

    # -------------------------------------------------------------------
    # OneDrive
    # -------------------------------------------------------------------
    if ( $RunOneDrive )
    {
        Write-Log "--- Starting OneDrive workload ---"
        Invoke-WithErrorHandling "OneDriveData"  { Get-OneDriveData  -Assessment $Assessment }
        Invoke-WithErrorHandling "OneDriveUsage" { Get-OneDriveUsage -Assessment $Assessment }
    }

    # -------------------------------------------------------------------
    # Teams
    # -------------------------------------------------------------------
    if ( $RunTeams )
    {
        Write-Log "--- Starting Teams workload ---"
        Invoke-WithErrorHandling "TeamsData"  { Get-TeamsData  -Assessment $Assessment }
        Invoke-WithErrorHandling "TeamsUsage" { Get-TeamsUsage -Assessment $Assessment }
    }

    # -------------------------------------------------------------------
    # Azure
    # -------------------------------------------------------------------
    if ( $RunAzure )
    {
        Write-Log "--- Starting Azure workload ---"
        Invoke-WithErrorHandling "AzureData" { Get-AzureData -Assessment $Assessment }
    }

    # -------------------------------------------------------------------
    # Copilot / AI
    # -------------------------------------------------------------------
    if ( $RunCopilot )
    {
        Write-Log "--- Starting Copilot / AI workload ---"
        Invoke-WithErrorHandling "CopilotData" { Get-CopilotData -Assessment $Assessment }
    }

    # -------------------------------------------------------------------
    # Purview
    # -------------------------------------------------------------------
    if ( $RunPurview )
    {
        Write-Log "--- Starting Purview workload ---"
        Invoke-WithErrorHandling "PurviewData" { Get-PurviewData -Assessment $Assessment }
    }

    # -------------------------------------------------------------------
    # Power Platform (stubbed - coming in a future release)
    # -------------------------------------------------------------------
    if ( $Workloads -contains "PowerPlatform" )
    {
        Write-Log "Power Platform collection is not yet implemented in this version." "WARN"
    }

    # -------------------------------------------------------------------
    # Write Output
    # -------------------------------------------------------------------
    $outputFile = Write-AssessmentOutput -Assessment $Assessment

    # -------------------------------------------------------------------
    # Done
    # -------------------------------------------------------------------
    Write-Host ""
    Write-Host "====================================================================" -ForegroundColor Cyan
    Write-Host " Assessment Complete" -ForegroundColor Green
    Write-Host "====================================================================" -ForegroundColor Cyan
    Write-Host ""
    Write-Host " Output saved to:" -ForegroundColor White
    Write-Host " $outputFile" -ForegroundColor Yellow
    Write-Host ""
    Write-Host " Please send the JSON and log file to your Rackspace Solutions Architect." -ForegroundColor White
    Write-Host ""
}

###################################################################################################
# Helper: Invoke-WithErrorHandling
# Wraps a workload scriptblock so a single failure does not abort the entire run.
###################################################################################################
function Invoke-WithErrorHandling
{
    param
    (
        [string]$Name,
        [scriptblock]$ScriptBlock
    )

    try
    {
            Write-Host $ScriptBlock
            Write-Log "--- Starting Device Management workload ---"
        & $ScriptBlock
    }
    catch
    {
        Write-Log "$Name failed unexpectedly: $( $_.Exception.Message )" "ERROR"
    }
}

###################################################################################################
# Helper: Write-AssessmentBanner
# Displays the intro disclaimer and countdown.
###################################################################################################
function Write-AssessmentBanner
{
    Write-Host ""
    Write-Host "=====================================================================" -ForegroundColor Cyan
    Write-Host " Microsoft 365 Quick Assessment (Read-Only) - Rackspace Technology"    -ForegroundColor Cyan
    Write-Host "=====================================================================" -ForegroundColor Cyan
    Write-Host ""
    Write-Host " This script performs READ-ONLY operations against your Microsoft 365 tenant."
    Write-Host " No changes will be made to your environment."
    Write-Host ""
    Write-Host " Workloads collected:"
    Write-Host " Entra ID, Exchange Online, SharePoint, OneDrive"
    Write-Host " Microsoft Teams, Device Management, Azure, Copilot, Purview"
    Write-Host ""
    Write-Host " Execution time will vary depending on tenant size:"
    Write-Host " ~1,000 users ~20 minutes"
    Write-Host " Larger tenants may take significantly longer"
    Write-Host ""
    Write-Host " Requirements:" -ForegroundColor DarkYellow
    Write-Host " PowerShell 7+"
    Write-Host " Microsoft Graph PowerShell SDK"
    Write-Host " Exchange Online Management Module"
    Write-Host " SharePoint Online Management Shell"
    Write-Host " Az.Accounts + Az.Resources"
    Write-Host ""
    Write-Host " Permissions Required (Read-Only):" -ForegroundColor DarkYellow
    Write-Host " Directory.Read.All Policy.Read.All"
    Write-Host " User.Read.All RoleManagement.Read.Directory"
    Write-Host " Reports.Read.All Group.Read.All"
    Write-Host " Team.ReadBasic.All Channel.ReadBasic.All"
    Write-Host " AuditLog.Read.All Files.Read.All"
    Write-Host " Sites.Read.All SecurityEvents.Read.All"
    Write-Host " DeviceManagementManagedDevices.Read.All"
    Write-Host " DeviceManagementConfiguration.Read.All"
    Write-Host " DeviceManagementServiceConfig.Read.All"
    Write-Host " DeviceManagementApps.Read.All"
    Write-Host ""
    Write-Host " Please ensure you authenticate with the correct tenant." -ForegroundColor DarkYellow
    Write-Host ""
    Write-Host "=====================================================================" -ForegroundColor Cyan

    Write-Host " What happens with your results?" -ForegroundColor Yellow
    Write-Host " Rackspace Technology specializes in Tenant to Tenant migrations and"
    Write-Host " Managed Services for your Microsoft 365 Eco system. Whether that is"
    Write-Host " on-prem to Microsoft 365 migrations, tenant-to-tenant migrations,"
    Write-Host " Copilot readiness, Entra ID, Exchange Online, Teams, and more."
    Write-Host " Send us your JSON report and someone will reach out to walk you"
    Write-Host " through the findings and discuss how we can help."
    Write-Host ""
    Write-Host "---------------------------------------------------------------------" -ForegroundColor DarkGray
    Write-Host " Legal Disclaimer:" -ForegroundColor Yellow
    Write-Host " This script is provided as-is without warranty of any kind."
    Write-Host " The author assumes no liability for any damages resulting from its use."
    Write-Host " All findings should be reviewed and validated prior to taking action."
    Write-Host "---------------------------------------------------------------------" -ForegroundColor DarkGray
    Write-Host ""

    Start-AssessmentCountdown -Seconds 10 -Message "Assessment will begin in"
}

###################################################################################################
# Helper: New-AssessmentSchema
# Returns the empty ordered hashtable that all workloads populate.
###################################################################################################
function New-AssessmentSchema
{
    return [ordered]@{

        Metadata = [ordered]@{
            TenantName           = $null
            TenantId             = $null
            AssessmentDate       = $null
            AssessmentVersion    = "2.0"
            SecureScore          = $null
            SecureScoreMax       = $null
            SecureScorePercent   = $null
        }

        Summary = [ordered]@{
            UserCount                = $null
            GuestUserCount           = $null
            GlobalAdminCount         = $null
            MFAEnabledPercent        = $null
            LegacyAuthEnabled        = $null
            IsHybridIdentity         = $null
            HasDirectorySync         = $null
            CustomDomainCount        = $null
            Mailboxes                = $null
            MailboxCount             = $null
            TotalMailboxSizeGB       = $null
            SharePointSiteCount      = $null
            SharePointTotalStorageGB = $null
            OneDriveCount            = $null
            OneDriveTotalStorageGB   = $null
            TeamCount                = $null
        }

        Licensing = [ordered]@{
            SkuSummary         = @()
            TotalLicensedUsers = $null
            UnlicensedUsers    = $null
        }

        Users = [ordered]@{
            ActiveUserCount   = $null
            InactiveUserCount = $null
        }

        Groups = [ordered]@{
            TotalGroups               = $null
            M365Groups                = $null
            SecurityGroups            = $null
            MailEnabledSecurityGroups = $null
            DistributionLists         = $null
            DynamicGroupsTotal        = $null
            DynamicM365Groups         = $null
            DynamicSecurityGroups     = $null
        }

        ConditionalAccess = [ordered]@{
            HasConditionalAccessPolicies = $null
            ConditionalAccessPolicyCount = $null
            HasMFAEnforcement            = $null
            HasTrustedLocations          = $null
            Policies                     = @()
        }

        DeviceManagement = [ordered]@{
            IntuneConfigured             = $null
            MDMConfigured                = $null
            HasAutopilot                 = $null
            AutopilotDeviceCount         = $null
            ManagedDeviceCount           = $null
            HasManagedDevices            = $null
            WindowsDeviceCount           = $null
            iOSDeviceCount               = $null
            AndroidDeviceCount           = $null
            macOSDeviceCount             = $null
            CompliantDeviceCount         = $null
            HasCompliantDevices          = $null
            HasHybridJoinedDevices       = $null
            HybridJoinedDeviceCount      = $null
            HasEntraJoinedDevices        = $null
            EntraJoinedDeviceCount       = $null
            ConfigurationPolicyCount     = $null
            CompliancePolicyCount        = $null
            DeviceProfileCount           = $null
            ManagedAppCount              = $null
            AppProtectionPolicyCount     = $null
            HasDefenderOnboardedDevices  = $null
            DefenderOnboardedDeviceCount = $null
        }

        Exchange = [ordered]@{
            MailboxCount                  = $null
            UserMailboxCount              = $null
            SharedMailboxCount            = $null
            ResourceMailboxCount          = $null
            LargeMailboxCount             = $null
            ArchiveMailboxCount           = $null
            ActiveMailboxCount            = $null
            TotalMailboxSizeGB            = $null
            HasConnectors                 = $null
            ConnectorCount                = $null
            HasTransportRules             = $null
            TransportRuleCount            = $null
            AcceptedDomainCount           = $null
            MailboxWithDelegatesCount     = $null
            HasExchangeRetentionPolicies  = $null
            IsExchangeHybrid              = $null
            HasRemoteMailboxes            = $null
            RemoteMailboxCount            = $null
            HasOnPremMailboxes            = $null
            MailContactCount              = $null
            MailUserCount                 = $null
            HasContacts                   = $null
            DistributionGroupCount        = $null
            MailEnabledSecurityGroupCount = $null
            DynamicDistributionGroupCount = $null
            HasDistributionLists          = $null
        }

        SharePoint = [ordered]@{
            SharePointSiteCount            = $null
            SharePointTotalStorageGB       = $null
            LargeSiteCount                 = $null
            ActiveSiteCount                = $null
            HasExternalSharing             = $null
            HasUniquePermissions           = $null
            HasSharePointRetentionPolicies = $null
            HasAppCatalog                  = $null
            HasSharePointCustomApps        = $null
            SharePointCustomAppCount       = $null
        }

        OneDrive = [ordered]@{
            OneDriveCount             = $null
            OneDriveTotalStorageGB    = $null
            LargeOneDriveCount        = $null
            ActiveOneDriveCount       = $null
            HasUnprovisionedOneDrives = $null
        }

        Teams = [ordered]@{
            TeamCount              = $null
            ActiveUserCount        = $null
            HasPrivateChannels     = $null
            PrivateChannelCount    = $null
            HasSharedChannels      = $null
            SharedChannelCount     = $null
            HasTeamsApps           = $null
            HasTeamsGuestAccess    = $null
            HasTeamsExternalAccess = $null
        }

        Applications = [ordered]@{
            HasEnterpriseApplications = $null
            CustomerOwnedApps         = $null
            ManagedIdentities         = $null
            HasAppRegistrations       = $null
            AppRegistrations          = $null
        }

        PowerPlatform = [ordered]@{
            HasPowerPlatform              = $null
            Licenses                      = $null
            PowerPlatformEnvironmentCount = $null
            HasDataverse                  = $null
            PowerAppCount                 = $null
            PowerAutomateFlowCount        = $null
            HasCustomConnectors           = $null
            HasDLPPolicies                = $null
        }

        Azure = [ordered]@{
            HasAzureSubscriptions  = $null
            AzureSubscriptionCount = $null
            HasSubscriptionAccess  = $null
        }

        Purview = [ordered]@{
            RetentionPolicies = $null
            RetentionLabels   = $null
            DlpPolicies       = $null
            SensitivityLabels = $null
            EdiscoveryCases   = $null
        }

        AI = [ordered]@{
            HasAILicenses   = $null
            AILicenseCount  = $null
            HasCopilot      = $null
            HasAIAgents     = $null
            AIAgentCount    = $null
            IsAIDataReady   = $null
            HasAIGovernance = $null
        }

        Findings = @()
    }
}

###################################################################################################
# Helper: Write-AssessmentOutput
# Cleans nulls and writes the final JSON file. Returns the output file path.
###################################################################################################
function Write-AssessmentOutput
{
    param ( $Assessment )

    $file = Join-Path $script:Context.OutputPath (
        "$( $script:Context.TenantPrefix )-$( $script:Context.WorkloadLabel )-$( $script:Context.RunStamp ).json"
    )

    $logFile = Join-Path $script:Context.OutputPath (
        "$( $script:Context.TenantPrefix )-$( $script:Context.WorkloadLabel )-$( $script:Context.RunStamp ).log"
    )

    Write-Log "Writing output to $file"

    $clean = Remove-NullProperties -Object $Assessment
    $clean | ConvertTo-Json -Depth 10 | Out-File $file -Encoding utf8

    Write-Log "Assessment output written successfully"

    return $file
}