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'
)