Modules/M365DSCCheckProperties.psm1

<#
.Description
    This function checks if properties of existing resources are up to date.
    Creates a report about missing or outdated properties of existing resources
    and a list of missing resources.
 
.Functionality
    Internal
#>


function Get-PropertyReport
{
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $DestinationFolder,

        [Parameter()]
        [System.Management.Automation.PSCredential]
        $Credential
    )

    # list of cmdlet parameters to be ignored
    $invalidParameters = @("ErrorVariable", `
                        "ErrorAction", `
                        "InformationVariable", `
                        "InformationAction", `
                        "WarningVariable", `
                        "WarningAction", `
                        "OutVariable", `
                        "OutBuffer", `
                        "PipelineVariable", `
                        "Verbose", `
                        "WhatIf", `
                        "Debug",
                        "Confirm",
                        "AsJob")

    # list of M365 DSC resource properties to be ignored
    $invalidProperties = @("ErrorVariable", `
                        "ErrorAction", `
                        "InformationVariable", `
                        "InformationAction", `
                        "WarningVariable", `
                        "WarningAction", `
                        "OutVariable", `
                        "OutBuffer", `
                        "PipelineVariable", `
                        "Verbose", `
                        "WhatIf", `
                        "Debug",
                        "Credential",
                        "ApplicationId",
                        "Ensure",
                        "TenantId",
                        "CertificateThumbprint",
                        "CertificatePath",
                        "CertificatePassword",
                        "IsSingleInstance")

    # list of M365 workloads to check
    $workloads = @(
        @{Name = 'ExchangeOnline'; ModuleName = "ExchangeOnlineManagement"; CommandName = "Get-Mailbox"; Prefix = "EXO"; }
        @{Name = 'MicrosoftTeams'; ModuleName = "MicrosoftTeams"; Prefix = "Teams"; }
        @{Name = 'SecurityComplianceCenter'; ModuleName = "ExchangeOnlineManagement"; CommandName = "Set-ComplianceCase"; Prefix = "SC"; }
    )

    # mapping table for resources with names different from cmdlet name
    $cmdletMapping = @{
        CasMailbox                   = "CASMailboxSettings"
        Mailbox                      = "SharedMailbox"
        MailboxRegionalConfiguration = "MailboxSettings"
        EXOPerimeterConfig           = "PerimeterConfiguration"
    }

    $missingResources = @()
    $report = @()

    if ($null -eq $Credential)
    {
        $Credential = Get-Credential
        $PSBoundParameters.Add("Credential", $Credential)
    }

    $folderPath = Join-Path $PSScriptRoot -ChildPath "../DSCResources"
    Write-Verbose "Folderpath of DSC resources: $folderPath"

    foreach ($module in $workloads)
    {
        Write-Verbose "Connecting to {$($Module.Name)}"
        $ConnectionMode = New-M365DSCConnection -Workload ($Module.Name) -InboundParameters $PSBoundParameters

        Write-Verbose "Getting list of cmdlets of {$($Module.ModuleName)}..."
        $CurrentModuleName = $Module.ModuleName

        if ($null -eq $CurrentModuleName -or $Module.CommandName)
        {
            Write-Verbose "Loading proxy for $($Module.ModuleName)"
            $foundModule = Get-Module | Where-Object -FilterScript { $_.ExportedCommands.Values.Name -ccontains $Module.CommandName }
            $CurrentModuleName = $foundModule.Name
            Import-Module $CurrentModuleName -Force -Global -ErrorAction SilentlyContinue
        }
        else
        {
            Import-Module $CurrentModuleName -Force -Global -ErrorAction SilentlyContinue
            $ConnectionMode = New-M365DSCConnection -Workload $Module.Name -InboundParameters $PSBoundParameters
        }

        $cmdlets = Get-Command -CommandType 'Function' -Module $CurrentModuleName
        $setCmdlets = $cmdlets | Where-Object { $_.Name -like 'Set-*' }

        Write-Verbose "Found $($setCmdlets.Count) Set-* cmdlets for $($Module.ModuleName) ($($cmdlets.Count) in total)"

        $i = 1
        foreach($cmdlet in $setCmdlets)
        {
            Write-Progress -Activity "Checking resources" -Status $cmdlet.Name -PercentComplete (($i / $setCmdlets.Length) * 100)

            $resourceExists = $false
            $resourceName = "MSFT_" + $module.Prefix + $cmdlet.Name.split('-')[1]

            if ($module.ModuleName -eq "MicrosoftTeams" -and $resourceName -like "*TeamsCsTeams*")
            {
                $resourceName = $resourceName -replace("TeamsCsTeams","Teams")
            }
            if ($module.ModuleName -eq "MicrosoftTeams" -and $resourceName -like "*TeamsCs*")
            {
                $resourceName = $resourceName -replace ("TeamsCs", "Teams")
            }
            $foundInFiles = Get-ChildItem -Path $folderPath | Where-Object { $_.Name -like $resourceName }

            if ($null -eq $foundInFiles)
            {
                $resourceNameFromMapping = $cmdletMapping[$cmdlet.Name.split('-')[1]]
                if ($null -ne $resourceNameFromMapping)
                {
                    $resourceName = "MSFT_" + $module.Prefix + $resourceNameFromMapping
                    $foundInFiles = Get-ChildItem -Path $folderPath | Where-Object { $_.Name -like $resourceName }
                    if ($null -ne $foundInFiles)
                    {
                        $resourceExists = $true
                    }
                }
            }
            else
            {
                $resourceExists = $true
            }

            if ($resourceExists)
            {
                # Get parameter of cmdlet
                Write-Verbose "Get parameters of cmdlet $($cmdlet.Name)"
                $targetParameters = @()
                $resourceParamters = @()
                $cmdletParameters = (Get-Command $cmdlet.Name).Parameters

                foreach ($parameter in $cmdletParameters.Keys)
                {
                    if ($parameter -notin $invalidParameters)
                    {
                        $targetParameters += $parameter
                    }
                }

                # Get properties of DSC resource
                Write-Verbose "Get properties of resource $resourceName"
                Import-Module $($folderPath + "\" + $resourceName) -Force
                $resourceProperties = (Get-Command Set-TargetResource -Module $resourceName).Parameters

                foreach ($property in $resourceProperties.Keys)
                {
                    if ($property -notin $invalidProperties)
                    {
                        $resourceParamters += $property
                    }
                }
                Remove-Module -Name $resourceName -Force -Confirm:$false

                # Compare properties
                Write-Verbose "Compare parameters of $resourceName"
                $difference = Compare-Object -ReferenceObject @($targetParameters | Select-Object) -DifferenceObject @($resourceParamters | Select-Object) -IncludeEqual
                $missingProperties = ($difference | Where-Object { $_.SideIndicator -eq "<="}).InputObject
                $addtionalProperties = ($difference | Where-Object { $_.SideIndicator -eq "=>" }).InputObject

                # Add to report
                $cmdletResult = [PSCustomObject]@{
                    "M365DSCResource"       = $resourceName
                    "Cmdlet"                = $cmdlet.Name
                    "Service"               = $module.Name
                    "MissingProperties"     = $missingProperties -join('; ')
                    "AdditionalProperties"  = $addtionalProperties -join('; ')
                }
                $report += $cmdletResult
            }
            else
            {
                $missingResources += $resourceName
                Write-Verbose "Resource $resourceName not found."
            }
            $i++
        }
    }

    # Export reports
    Write-Verbose "Export reports"
    $report | Export-Csv -NoTypeInformation -Path "$DestinationFolder\M365DSC-Properties-Report.csv" -Delimiter ","
    $missingResources | Out-File "$DestinationFolder\MissingDSCResources.csv"
}

Export-ModuleMember -Function @(
    'Get-PropertyReport'
)