Modules/M365DSCCheckProperties.psm1

<#
.SYNOPSIS
    Checks if properties of existing resources are up to date.
 
.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.
 
.PARAMETER DestinationFolder
    Specifies the destination folder where the report will be saved.
 
.PARAMETER Credential
    Specifies the credentials to use for connecting to Microsoft 365 workloads.
 
.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',
        'Force',
        'HttpPipelinePrepend',
        'InformationVariable',
        'InformationAction',
        'MsftInternalProcessingMode',
        'WarningVariable',
        'WarningAction',
        'OutVariable',
        'OutBuffer',
        'PipelineVariable',
        'Verbose',
        'WhatIf',
        'Debug',
        'Confirm',
        'AsJob'
    )

    # list of M365 DSC resource properties to be ignored
    $invalidProperties = @(
        'ErrorVariable',
        'ErrorAction',
        'Force',
        'HttpPipelinePrepend',
        'InformationVariable',
        'InformationAction',
        'MsftInternalProcessingMode',
        'WarningVariable',
        'WarningAction',
        'OutVariable',
        'OutBuffer',
        'PipelineVariable',
        'Verbose',
        'WhatIf',
        'Debug',
        'Confirm',
        'AsJob'
        'Credential',
        'ApplicationId',
        'ApplicationSecret',
        'Ensure',
        'TenantId',
        'CertificateThumbprint',
        'CertificatePath',
        'CertificatePassword',
        'AccessTokens',
        'ManagedIdentity',
        'IsSingleInstance'
    )

    # list of M365 workloads to check
    $workloads = @(
        @{Name = 'MicrosoftTeams'; ModuleName = 'MicrosoftTeams'; Prefix = 'Teams'; }
        @{Name = 'ExchangeOnline'; ModuleName = 'ExchangeOnlineManagement'; CommandName = 'Get-Mailbox'; Prefix = 'EXO'; }
        @{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)
    {
        if ($module.Name -ne 'MicrosoftTeams')
        {
            Write-Verbose "Connecting to {$($Module.Name)}"
            $null = 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
            $null = New-M365DSCConnection -Workload $Module.Name -InboundParameters $PSBoundParameters
        }

        $cmdlets = Get-Command -CommandType 'Function' -Module $CurrentModuleName
        $setCmdlets = $cmdlets | Where-Object -Property 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 -Property 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 -Property 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 = @()
                $resourceParameters = @()
                $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)
                    {
                        $resourceParameters += $property
                    }
                }
                Remove-Module -Name $resourceName -Force -Confirm:$false

                # Compare properties
                Write-Verbose "Compare parameters of $resourceName"
                $difference = Compare-Object -ReferenceObject @($targetParameters | Select-Object) -DifferenceObject @($resourceParameters | Select-Object) -IncludeEqual
                $missingProperties = ($difference | Where-Object -Property SideIndicator -EQ '<=' ).InputObject
                $additionalProperties = ($difference | Where-Object -Property SideIndicator -EQ '=>' ).InputObject

                # Add to report
                $cmdletResult = [PSCustomObject]@{
                    'M365DSCResource'      = $resourceName
                    'Cmdlet'               = $cmdlet.Name
                    'Service'              = $module.Name
                    'MissingProperties'    = $missingProperties -join ('; ')
                    'AdditionalProperties' = $additionalProperties -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'
)