Modules/M365DSCCompare.psm1

<#
.SYNOPSIS
    This module contains the comparison logic for M365DSC.
#>

function Compare-M365DSCResourceState
{
    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $ResourceName,

        [Parameter(Mandatory = $true)]
        [System.Collections.Hashtable]
        $DesiredValues,

        [Parameter(Mandatory = $true)]
        [System.Collections.Hashtable]
        $CurrentValues,

        [Parameter()]
        [System.String[]]
        $ExcludedProperties,

        [Parameter()]
        [System.String[]]
        $IncludedProperties,

        [Parameter()]
        [System.Func[Hashtable, Hashtable, Hashtable, [Object[]], Tuple[Hashtable, Hashtable, Hashtable]]]
        $PostProcessing,

        [Parameter()]
        [System.Object[]]
        $PostProcessingArgs = @()
    )

    $Global:AllDrifts = @{
        DriftInfo     = @()
        CurrentValues = @{}
        DesiredValues = @{}
    }
    $Global:PotentialDrifts = @()

    # Retrieve the primary keys of the given resource and remove them from the list of values to check.
    $currentPath = $PSScriptRoot
    if ($null -eq $Script:M365DSCSchema)
    {
        $schemaPath = Join-Path -Path $currentPath -ChildPath '..\SchemaDefinition.json'
        $schemaJSON = Get-Content $schemaPath -Raw
        $Script:M365DSCSchema = ConvertFrom-Json $schemaJSON
    }
    $resourceDefinition = $Script:M365DSCSchema | Where-Object -FilterScript { $_.ClassName -eq "MSFT_$ResourceName" }
    $resourceKeys = $resourceDefinition.Parameters | Where-Object -FilterScript { $_.Option -eq 'Key' }

    $ValuesToCheck = $DesiredValues.Clone()

    # Apply custom post-processing to CurrentValues and ValuesToCheck if specified
    if ($null -ne $PostProcessing)
    {
        Write-Verbose -Message "Applying custom post-processing to CurrentValues and ValuesToCheck for resource $ResourceName"
        try
        {
            $result = $PostProcessing.Invoke($DesiredValues, $CurrentValues, $ValuesToCheck, $PostProcessingArgs)
            if ($null -ne $result -and $result.Item1 -is [Hashtable] -and $result.Item2 -is [Hashtable] -and $result.Item3 -is [Hashtable])
            {
                $DesiredValues = $result.Item1
                $CurrentValues = $result.Item2
                $ValuesToCheck = $result.Item3
            }
            else
            {
                Write-Warning -Message "PostProcessing function did not return a valid tuple for resource $ResourceName. Using original values."
            }
        }
        catch
        {
            Write-Warning -Message "Error occurred during post-processing for resource $ResourceName`: $_"
        }
    }

    $ValuesToCheck.Remove('Id') | Out-Null
    $ValuesToCheck.Remove('Identity') | Out-Null

    # Remove the key parameters from the comparison
    foreach ($keyToRemove in $resourceKeys)
    {
        $ValuesToCheck.Remove($keyToRemove.Name) | Out-Null
    }

    # Remove PSCredential object from the list of properties to be evaluated
    $credentialProperties = $resourceDefinition.Parameters | Where-Object -FilterScript { $_.CIMType -eq 'MSFT_Credential' }
    foreach ($property in $credentialProperties)
    {
        $ValuesToCheck.Remove($property.Name) | Out-Null
    }

    # Remove the ExcludedProperties from the list of properties to be evaluated
    foreach ($property in $ExcludedProperties)
    {
        $ValuesToCheck.Remove($property) | Out-Null
    }

    # Add the IncludedProperties to the list of properties to be evaluated
    foreach ($property in $IncludedProperties)
    {
        if ($DesiredValues.ContainsKey($property) -and -not $ValuesToCheck.ContainsKey($property))
        {
            $ValuesToCheck.Add($property, $DesiredValues.$property)
        }
    }

    $testTargetResource = $true
    $skipEvaluation = $false
    if ($DesiredValues.Ensure -eq 'Present' -and $CurrentValues.Ensure -eq 'Absent')
    {
        Write-Verbose -Message "The resource $ResourceName with $finalString was not found in the tenant."
        $Global:AllDrifts.DriftInfo += @{
            PropertyName = 'Ensure'
            CurrentValue = 'Absent'
            DesiredValue = 'Present'
        }
        $testTargetResource = $false
    }
    elseif ($DesiredValues.Ensure -eq 'Absent' -and $CurrentValues.Ensure -eq 'Present')
    {
        Write-Verbose -Message "The resource $ResourceName with $finalString should not exist in the tenant."
        $Global:AllDrifts.DriftInfo += @{
            PropertyName = 'Ensure'
            CurrentValue = 'Present'
            DesiredValue = 'Absent'
        }
        $testTargetResource = $false
    }
    elseif ($DesiredValues.Ensure -eq 'Absent' -and $CurrentValues.Ensure -eq 'Absent')
    {
        Write-Verbose -Message "The resource $ResourceName with $finalString does not exist in the tenant as desired." -Verbose:$Verbose
        $skipEvaluation = $true
    }

    $testResult = $true
    if ($testTargetResource -and -not $skipEvaluation)
    {
        # Compare Cim instances
        $desiredKeys = $DesiredValues.Clone().Keys
        foreach ($key in $desiredKeys)
        {
            $source = $DesiredValues.$key
            $target = $CurrentValues.$key
            $parameterDefinition = $resourceDefinition.Parameters | Where-Object -FilterScript { $_.Name -eq $key }
            if ($null -ne $source -and ($source.GetType().Name -like '*CimInstance*' -or $parameterDefinition.CIMType -like "MSFT_*"))
            {
                Write-Verbose -Message "Comparing complex object property $key of resource $ResourceName"
                $CIMProperty = $parameterDefinition
                $CIMName = $CIMProperty.CIMType.Replace('[]', '')
                $CIMDefinition = $Script:M365DSCSchema | Where-Object -FilterScript { $_.ClassName -eq $CIMName }
                $CIMPrimaryKeys = $CIMDefinition.Parameters | Where-Object -FilterScript { $_.Option -eq 'Required' }

                $targetObjects = @{}
                if ($source.GetType().Name -in @('CimInstance[]', 'Object[]'))
                {
                    $targetObjects = @()
                }

                # Filter all target objects that match the primary keys of the source object(s)
                $target = $target | Where-Object -FilterScript {
                    $match = $true
                    foreach ($primaryKey in $CIMPrimaryKeys.Name)
                    {
                        # Because $source can be an array, we need to check if the
                        # primary key value exists in any of the source objects
                        $sourceValue = $source.$primaryKey | Select-Object -Unique
                        if ($_.$primaryKey -notin @($sourceValue))
                        {
                            $match = $false
                        }
                    }
                    return $match
                }

                foreach ($targetObject in $target)
                {
                    foreach ($primaryKey in $CIMPrimaryKeys.Name)
                    {
                        if ($primaryKey -notin $IncludedProperties)
                        {
                            $targetObject.Remove($primaryKey) | Out-Null
                        }
                    }

                    if ($targetObjects -is [array])
                    {
                        $targetObjects += $targetObject
                    }
                    else
                    {
                        $targetObjects = $targetObject
                    }
                }

                $testResult = Compare-M365DSCComplexObject `
                    -Source ($source) `
                    -Target ($targetObjects) `
                    -PropertyName $key

                if (-not $testResult)
                {
                    Write-Verbose "TestResult returned False for $source"
                    $testTargetResource = $false
                }

                $DesiredValues.Remove($key) | Out-Null
                $ValuesToCheck.Remove($key) | Out-Null
            }
        }
    }

    Write-Verbose -Message "Current Values: $(Convert-M365DscHashtableToString -Hashtable $CurrentValues)"
    Write-Verbose -Message "Target Values: $(Convert-M365DscHashtableToString -Hashtable $ValuesToCheck)"

    $testResult2 = $true
    if (-not $skipEvaluation)
    {
        $testResult2 = Test-M365DSCParameterState -CurrentValues $CurrentValues `
            -Source $ResourceName `
            -DesiredValues $DesiredValues `
            -ValuesToCheck $ValuesToCheck.Keys `
            -NoEventMessage `
            -NoDriftReset
    }

    if ($testResult -and -not $testResult2)
    {
        $testResult = $false
    }

    if (-not $testResult)
    {
        $testTargetResource = $false
    }

    return $testTargetResource
}

Export-ModuleMember -Function Compare-M365DSCResourceState