M365DSC.CRG.psm1

#Region './Private/Convert-Indent.ps1' -1

function Convert-Indent
{
    <#
        .Synopsis
        Converts a numbered indentation into spaces or tabs

        .Description
        This function converts a numbered indentation into spaces or tabs.

        .Example
        Convert-Ident -Indent 2

        .Parameter Indent
        The Indent parameter is the number of spaces or tabs that need to be returned.
    #>

    [CmdletBinding()]
    [OutputType([String])]
    param
    (
        [Parameter()]
        [System.String]
        $Indent
    )

    process
    {
        switch ($Indent)
        {
            ''
            {
                return ' '
            }
            '1'
            {
                return "`t"
            }
            '2'
            {
                return ' '
            }
            '4'
            {
                return ' '
            }
            '0'
            {
                return ''
            }
        }
        $Indent
    }
}
#EndRegion './Private/Convert-Indent.ps1' 53
#Region './Private/ConvertTo-Psd.ps1' -1

function ConvertTo-Psd
{
    <#
        .Synopsis
        Converts the inputted object to a hashtable in string format, which can be saved as PSD.

        .Description
        This function converts the inputted object to a string format, which can then be saved to a PSD1 file.

        .Example
        $configData = @{
            Value1 = "String1"
            Value2 = 25
            Value3 = @{
                Value4 = "String2"
                Value5 = 50
            }
            Value6 = @(
                @{
                    Value7 = "String3"
                    Value8 = 75
                }
            )
        }
        $configData | ConvertTo-Psd

        .Parameter InputObject
        The InputObject parameter specified the object that has to be converted into PSD format.

        .Parameter Depth
        The Depth parameter specifies how deep the recursion should go.

        .Parameter Indent
        The Indent parameter is the number of spaces that need to be indented.
    #>

    [CmdletBinding()]
    [OutputType([String])]
    param (
        [Parameter(Position = 0, ValueFromPipeline = 1)]
        [System.Object]
        $InputObject,

        [Parameter()]
        [System.Int32]
        $Depth,

        [Parameter()]
        [System.String]
        $Indent
    )

    begin
    {
        $objects = [System.Collections.Generic.List[object]]@()
    }

    process
    {
        $objects.Add($InputObject)
    }

    end
    {
        trap
        {
            Invoke-TerminatingError $_
        }

        $script:Depth = $Depth
        $script:Pruned = 0
        $script:Indent = Convert-Indent -Indent $Indent
        $script:Writer = New-Object System.IO.StringWriter
        try
        {
            foreach ($object in $objects)
            {
                Write-Psd -Object $object
            }
            $script:Writer.ToString().TrimEnd()
            if ($script:Pruned)
            {
                Write-Warning "ConvertTo-Psd truncated $script:Pruned objects."
            }
        }
        finally
        {
            $script:Writer = $null
        }
    }
}
#EndRegion './Private/ConvertTo-Psd.ps1' 91
#Region './Private/Get-AttributeString.ps1' -1

function Get-AttributeString
{
    <#
        .Synopsis
        Adds all the provided properties to the configuration data object.

        .Description
        This function loops through all the provided properties and adds them to the specified configuration data object.

        .Example
        Get-AttributeString -Property $property -ConfigData $currentDataObject

        .Parameter Property
        The Property parameter is a property from the MOF schema that needs to be added to the configuration data object.

        .Parameter ConfigData
        The ConfigData parameter is the configuration data object that needs to be updated with the provided property.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.Object]
        $Property,

        [Parameter(Mandatory = $true)]
        [System.Object]
        $ConfigData
    )

    process
    {
        $script:currentDepth++

        if ($script:currentDepth -gt $script:maxDepth)
        {
            Write-Verbose 'MaxDepth reached!'
            $script:currentDepth--
            return
        }

        # Property is an EmbeddedInstance, so we have to use that in generating the properties.
        $embeddedSchemas = $mofSchemas |
            Where-Object -FilterScript {
                    ($_.ClassName -eq $Property.EmbeddedInstance)
            }

        Write-Verbose "CurrentDepth: $($script:currentDepth)"

        if ($Property.IsArray)
        {
            $ConfigData.$($Property.Name) = @(@{})

            if ($embeddedSchemas.Attributes.Name -notcontains "Id" -and $embeddedSchemas.Attributes.Name -notcontains "Identity")
            {
                $ConfigData.$($property.Name)[0].UniqueId = ('{0} | {1} | {2}' -f "String", "Required", "[Unique ID to identify this specific object]")
            }

            foreach ($embeddedProperty in $embeddedSchemas.Attributes)
            {
                if ($null -eq $embeddedProperty.EmbeddedInstance)
                {
                    $state = 'Optional'
                    if ($embeddedProperty.State -in @('Key', 'Required'))
                    {
                        $state = 'Required'
                    }

                    if ($null -eq $embeddedProperty.ValueMap)
                    {
                        if ($embeddedProperty.DataType -like "*Array")
                        {
                            $dataType = $embeddedProperty.DataType -replace "Array"
                            $result = @(('{0} | {1} | {2}' -f $dataType, $state, $embeddedProperty.Description))
                        }
                        else
                        {
                            $result = ('{0} | {1} | {2}' -f $embeddedProperty.DataType, $state, $embeddedProperty.Description)
                        }
                    }
                    else
                    {
                        if ($embeddedProperty.DataType -like "*Array")
                        {
                            $dataType = $embeddedProperty.DataType -replace "Array"
                            $result = @(('{0} | {1} | {2} | {3}' -f $dataType, $state, $embeddedProperty.Description, ($embeddedProperty.ValueMap -join ' / ')))
                        }
                        else
                        {
                            $result = ('{0} | {1} | {2} | {3}' -f $embeddedProperty.DataType, $state, $embeddedProperty.Description, ($embeddedProperty.ValueMap -join ' / '))
                        }
                    }
                    $ConfigData.$($property.Name)[0].$($embeddedProperty.Name) = $result
                }
                else
                {
                    Get-AttributeString -Property $embeddedProperty -ConfigData ($ConfigData.$($Property.Name)[0])
                }
            }
        }
        else
        {
            $ConfigData.$($property.Name) = @{}

            foreach ($embeddedProperty in $embeddedSchemas.Attributes)
            {
                if ($null -eq $embeddedProperty.EmbeddedInstance)
                {
                    $state = 'Optional'
                    if ($embeddedProperty.State -in @('Key', 'Required'))
                    {
                        $state = 'Required'
                    }

                    if ($null -eq $embeddedProperty.ValueMap)
                    {
                        if ($embeddedProperty.DataType -like "*Array")
                        {
                            $dataType = $embeddedProperty.DataType -replace "Array"
                            $result = @(('{0} | {1} | {2}' -f $dataType, $state, $embeddedProperty.Description))
                        }
                        else
                        {
                            $result = ('{0} | {1} | {2}' -f $embeddedProperty.DataType, $state, $embeddedProperty.Description)
                        }
                    }
                    else
                    {
                        if ($embeddedProperty.DataType -like "*Array")
                        {
                            $dataType = $embeddedProperty.DataType -replace "Array"
                            $result = @(('{0} | {1} | {2} | {3}' -f $dataType, $state, $embeddedProperty.Description, ($embeddedProperty.ValueMap -join ' / ')))
                        }
                        else
                        {
                            $result = ('{0} | {1} | {2} | {3}' -f $embeddedProperty.DataType, $state, $embeddedProperty.Description, ($embeddedProperty.ValueMap -join ' / '))
                        }
                    }
                    $ConfigData.$($property.Name).$($embeddedProperty.Name) = $result
                }
                else
                {
                    Get-AttributeString -Property $embeddedProperty -ConfigData ($ConfigData.$($Property.Name))
                }
            }
        }
        $script:currentDepth--
    }
}
#EndRegion './Private/Get-AttributeString.ps1' 151
#Region './Private/Get-EmbeddedPropertyString.ps1' -1

function Get-EmbeddedPropertyString
{
    <#
        .Synopsis
        Adds all the provided embedded properties to the composite resource code.

        .Description
        This function loops through all the provided properties and adds references to them to the generated composite resource.

        .Example
        $result = Get-EmbeddedPropertyString -Properties $embeddedSchema.Attributes -Indentation $Indentation -ParameterName "`$_"

        .Parameter Properties
        The Properties parameter are the properties from the MOF schema that needs to be added to the composite resource.

        .Parameter Indentation
        The Indentation parameter is number of indentations that should be used for the generated code.

        .Parameter ParameterName
        The ParameterName parameter is the name of the parameter that should be used for the generated code.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.Object]
        $Properties,

        [Parameter(Mandatory = $true)]
        [System.Int32]
        $Indentation,

        [Parameter(Mandatory = $true)]
        [System.String]
        $ParameterName
    )

    process
    {
        $script:currentDepth++

        if ($script:currentDepth -gt $script:maxDepth)
        {
            Write-Verbose 'MaxDepth reached!'
            $script:currentDepth--
            return
        }

        $propertyString = [System.Text.StringBuilder]::new()

        $embeddedProperties = $Properties | Where-Object { $null -ne $_.EmbeddedInstance -and $_.EmbeddedInstance -ne 'MSFT_Credential' }

        if ($embeddedProperties.Count -ne 0)
        {
            Write-Verbose "CurrentDepth: $($script:currentDepth)"
            foreach ($Property in $embeddedProperties)
            {
                Write-Verbose "$($Property.Name) - $($Property.EmbeddedInstance)"
                $embeddedSchema = $mofSchemas |
                    Where-Object -FilterScript {
                            ($_.ClassName -eq $Property.EmbeddedInstance)
                    }

                if ($null -ne $embeddedSchema)
                {
                    $propertyName = $Property.Name
                    [void]$propertyString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $Indentation), "if ($ParameterName.ContainsKey('$propertyName'))"))
                    [void]$propertyString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $Indentation), '{'))
                    $Indentation++

                    [void]$propertyString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $Indentation), "$ParameterName.$propertyName = $ParameterName.$propertyName | ForEach-Object {"))
                    $Indentation++

                    $result = Get-EmbeddedPropertyString -Properties $embeddedSchema.Attributes -Indentation $Indentation -ParameterName "`$_"
                    if ([String]::IsNullOrEmpty($result) -eq $false)
                    {
                        [void]$propertyString.Append($result)
                    }

                    [void]$propertyString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $Indentation), "if (`$_.ContainsKey('UniqueId'))"))
                    [void]$propertyString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $Indentation), '{'))
                    [void]$propertyString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $Indentation), " `$_.Remove('UniqueId')"))
                    [void]$propertyString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $Indentation), '}'))
                    [void]$propertyString.AppendLine('')

                    [void]$propertyString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $Indentation), "(Get-DscSplattedResource -ResourceName '$($Property.EmbeddedInstance)' -Properties `$_ -NoInvoke).Invoke(`$_)"))

                    $Indentation--
                    [void]$propertyString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $Indentation), '}'))

                    $Indentation--
                    [void]$propertyString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $Indentation), '}'))
                    [void]$propertyString.AppendLine('')
                }
                else
                {
                    Write-Warning -Message "Specified embedded instance '$($Property.EmbeddedInstance)' for property '$($Property.Name)' cannot be found!"
                    $script:errors += [PSCustomObject]@{
                        Type = 'Warning'
                        Message = "Specified embedded instance '$($Property.EmbeddedInstance)' for property '$($Property.Name)' cannot be found for resource '$shortResourceName'!"
                    }
                }
            }
        }

        $script:currentDepth--
        return $propertyString.ToString()
    }
}
#EndRegion './Private/Get-EmbeddedPropertyString.ps1' 111
#Region './Private/Get-IndentationString.ps1' -1

function Get-IndentationString
{
    <#
      .SYNOPSIS
      Converts a number into a number of spaces, used for indentation.

      .DESCRIPTION
      This function converts a number into a number of spaces (4x the provided number), so it can be used for indentation.

      .EXAMPLE
      Get-IndentationString -Indentation 2

      .PARAMETER Indentation
      The number of indentations (four spaces) that should be returned.
    #>

    [CmdletBinding()]
    [OutputType([String])]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.Int32]
        $Indentation
    )

    process
    {
        return (' ' * $Indentation)
    }
}
#EndRegion './Private/Get-IndentationString.ps1' 30
#Region './Private/Get-MofSchemaObject.ps1' -1

function Get-MofSchemaObject
{
    <#
        .Synopsis
        Reads and parses the Mof schema file

        .Description
        This function reads a Mof schema file and parses the results into
        an object.

        .Parameter FileName
        Specifies the full path of the Mof schema file.

        .Example
        # Loops through all schemas in the Microsoft365DSC module
        $m365modulePath = Split-Path -Path (Get-Module Microsoft365DSC -ListAvailable) -Parent
        $mofSearchPath = Join-Path -Path $m365modulePath -ChildPath '\**\**.schema.mof'
        $mofSchemaFiles = @(Get-ChildItem -Path $mofSearchPath -Recurse)
        foreach ($mofSchemaFile in $mofSchemaFiles)
        {
            $mofSchemas = Get-MofSchemaObject -FileName $mofSchemaFile.FullName
            # Your code to use the schemas
        }
    #>

    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable])]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $FileName
    )

    process
    {
        if ($IsMacOS)
        {
            throw 'NotImplemented: Currently there is an issue using the type [Microsoft.PowerShell.DesiredStateConfiguration.Internal.DscClassCache] on macOS. See issue https://github.com/PowerShell/PowerShell/issues/5970 and issue https://github.com/PowerShell/MMI/issues/33.'
        }

        $temporaryPath = Get-TemporaryPath

        #region Workaround for OMI_BaseResource inheritance not resolving.
        $filePath = (Resolve-Path -Path $FileName).Path
        $tempFilePath = Join-Path -Path $temporaryPath -ChildPath "DscMofHelper_$((New-Guid).Guid).tmp"
        $rawContent = (Get-Content -Path $filePath -Raw) -replace '\s*:\s*OMI_BaseResource'

        Set-Content -LiteralPath $tempFilePath -Value $rawContent -ErrorAction 'Stop'

        # .NET methods don't like PowerShell drives
        $tempFilePath = Convert-Path -Path $tempFilePath
        #endregion

        try
        {
            $exceptionCollection = [System.Collections.ObjectModel.Collection[System.Exception]]::new()
            $moduleInfo = [System.Tuple]::Create('Module', [System.Version] '1.0.0')

            $class = [Microsoft.PowerShell.DesiredStateConfiguration.Internal.DscClassCache]::ImportClasses(
                $tempFilePath, $moduleInfo, $exceptionCollection
            )
        }
        catch
        {
            throw "Failed to import classes from file $FileName. Error $_"
        }
        finally
        {
            Remove-Item -LiteralPath $tempFilePath -Force
        }

        foreach ($currentCimClass in $class)
        {
            $attributes = foreach ($property in $currentCimClass.CimClassProperties)
            {
                $state = switch ($property.flags)
                {
                    { $_ -band [Microsoft.Management.Infrastructure.CimFlags]::Key }
                    {
                        'Key'
                    }
                    { $_ -band [Microsoft.Management.Infrastructure.CimFlags]::Required }
                    {
                        'Required'
                    }
                    { $_ -band [Microsoft.Management.Infrastructure.CimFlags]::ReadOnly }
                    {
                        'Read'
                    }
                    default
                    {
                        'Write'
                    }
                }

                @{
                    Name             = $property.Name
                    State            = $state
                    DataType         = $property.CimType
                    ValueMap         = $property.Qualifiers.Where( { $_.Name -eq 'ValueMap' }).Value
                    IsArray          = $property.CimType -gt 16
                    Description      = $property.Qualifiers.Where( { $_.Name -eq 'Description' }).Value
                    EmbeddedInstance = $property.Qualifiers.Where( { $_.Name -eq 'EmbeddedInstance' }).Value
                }
            }

            @{
                ClassName    = $currentCimClass.CimClassName
                Attributes   = $attributes
                ClassVersion = $currentCimClass.CimClassQualifiers.Where( { $_.Name -eq 'ClassVersion' }).Value
                FriendlyName = $currentCimClass.CimClassQualifiers.Where( { $_.Name -eq 'FriendlyName' }).Value
            }
        }
    }
}
#EndRegion './Private/Get-MofSchemaObject.ps1' 116
#Region './Private/Get-TemporaryPath.ps1' -1

function Get-TemporaryPath
{
    <#
        .Synopsis
        Gets the temporary path for the operating system

        .Description
        This function retrieves the system temporary path for the operating system.

        .Example
        Get-TemporaryPath
    #>

    [CmdletBinding()]
    [OutputType([System.String])]
    param ()

    process
    {
        $temporaryPath = $null

        switch ($true)
        {
            (-not (Test-Path -Path variable:IsWindows) -or ((Get-Variable -Name 'IsWindows' -ValueOnly -ErrorAction SilentlyContinue) -eq $true))
            {
                # Windows PowerShell or PowerShell 6+
                $temporaryPath = (Get-Item -Path env:TEMP).Value
            }

            ((Get-Variable -Name 'IsMacOs' -ValueOnly -ErrorAction SilentlyContinue) -eq $true)
            {
                $temporaryPath = (Get-Item -Path env:TMPDIR).Value
            }

            ((Get-Variable -Name 'IsLinux' -ValueOnly -ErrorAction SilentlyContinue) -eq $true)
            {
                $temporaryPath = '/tmp'
            }

            default
            {
                throw 'Cannot set the temporary path. Unknown operating system.'
            }
        }

        return $temporaryPath
    }
}
#EndRegion './Private/Get-TemporaryPath.ps1' 48
#Region './Private/Initialize-Module.ps1' -1

function Initialize-Module
{
    <#
        .Synopsis
        Initializes the module by creating all required files and folders

        .Description
        This function initializes the M365DSC.CompositeResources module by creating the
        required folder structure and the module manifest file.

        .Parameter Version
        Specifies the version of the module.

        .Parameter OutputPath
        Path to which files of the new module are written.

        .Example
        Initialize-Module -Version 1.23.1122.100
    #>

    [CmdletBinding()]
    [OutputType()]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $Version,

        [Parameter(Mandatory = $true)]
        [System.String]
        $OutputPath
    )

    process
    {
        # Create the folder structure
        $modulePath = Join-Path -Path $OutputPath -ChildPath "M365DSC.CompositeResources\$Version"
        if ((Test-Path -Path $modulePath) -eq $false)
        {
            $null = New-Item -Path $modulePath -ItemType Directory
        }

        $dscResourcesPath = Join-Path -Path $modulePath -ChildPath 'DscResources'
        if ((Test-Path -Path $dscResourcesPath) -eq $false)
        {
            $null = New-Item -Path $dscResourcesPath -ItemType Directory
        }

        $m365dscVersion = $Version.Substring(0, $Version.LastIndexOf('.') + 2)

        # Create the module manifest content
        $moduleManifestString = [System.Text.StringBuilder]::new()
        [void]$moduleManifestString.AppendLine('#')
        [void]$moduleManifestString.AppendLine("# Module manifest for module 'M365DSC.CompositeResources'")
        [void]$moduleManifestString.AppendLine('#')
        [void]$moduleManifestString.AppendLine("# Generated on: $(Get-Date -f "d-M-yyyy")")
        [void]$moduleManifestString.AppendLine('#')
        [void]$moduleManifestString.AppendLine('')
        [void]$moduleManifestString.AppendLine('@{')
        [void]$moduleManifestString.AppendLine(' # Script module or binary module file associated with this manifest.')
        [void]$moduleManifestString.AppendLine(" RootModule = 'M365DSC.CompositeResources.psm1'")
        [void]$moduleManifestString.AppendLine('')
        [void]$moduleManifestString.AppendLine(' # Version number of this module.')
        [void]$moduleManifestString.AppendLine(" ModuleVersion = '$Version'")
        [void]$moduleManifestString.AppendLine('')
        [void]$moduleManifestString.AppendLine(' # ID used to uniquely identify this module')
        [void]$moduleManifestString.AppendLine(" GUID = '8c07a295-6a8d-465d-933d-9f598d77fdfb'")
        [void]$moduleManifestString.AppendLine('')
        [void]$moduleManifestString.AppendLine(' # Author of this module')
        [void]$moduleManifestString.AppendLine(" Author = 'Yorick Kuijs'")
        [void]$moduleManifestString.AppendLine('')
        [void]$moduleManifestString.AppendLine(' # Company or vendor of this module')
        [void]$moduleManifestString.AppendLine(" CompanyName = 'Microsoft'")
        [void]$moduleManifestString.AppendLine('')
        [void]$moduleManifestString.AppendLine(' # Modules that must be imported into the global environment prior to importing this module')
        [void]$moduleManifestString.AppendLine(' RequiredModules = @(')
        [void]$moduleManifestString.AppendLine(" @{ModuleName='DscBuildHelpers'; ModuleVersion ='0.2.1'; GUID='23ccd4bf-0a52-4077-986f-c153893e5a6a'}")
        [void]$moduleManifestString.AppendLine(" @{ModuleName='Microsoft365DSC'; RequiredVersion='$m365dscVersion'; GUID='39f599a6-d212-4480-83b3-a8ea2124d8cf'}")
        [void]$moduleManifestString.AppendLine(' )')
        [void]$moduleManifestString.AppendLine('')
        [void]$moduleManifestString.AppendLine(' # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export.')
        [void]$moduleManifestString.AppendLine(" FunctionsToExport = @('New-M365DSCExampleDataFile')")
        [void]$moduleManifestString.AppendLine('')
        [void]$moduleManifestString.AppendLine(' # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export.')
        [void]$moduleManifestString.AppendLine(" CmdletsToExport = @()")
        [void]$moduleManifestString.AppendLine('')
        [void]$moduleManifestString.AppendLine(' # Variables to export from this module')
        [void]$moduleManifestString.AppendLine(" VariablesToExport = @()")
        [void]$moduleManifestString.AppendLine('')
        [void]$moduleManifestString.AppendLine(' # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export.')
        [void]$moduleManifestString.AppendLine(" AliasesToExport = @()")
        [void]$moduleManifestString.AppendLine('')
        [void]$moduleManifestString.AppendLine(' # DSC resources to export from this module')
        [void]$moduleManifestString.AppendLine(" DscResourcesToExport = @('*')")
        [void]$moduleManifestString.AppendLine('')
        [void]$moduleManifestString.AppendLine(' # Description of the functionality provided by this module')
        [void]$moduleManifestString.AppendLine(" Description = 'DSC composite resource for configuring Microsoft 365'")
        [void]$moduleManifestString.AppendLine('')
        [void]$moduleManifestString.AppendLine(' # Minimum version of the Windows PowerShell engine required by this module')
        [void]$moduleManifestString.AppendLine(" PowerShellVersion = '5.0'")
        [void]$moduleManifestString.AppendLine('')
        [void]$moduleManifestString.AppendLine(' # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell.')
        [void]$moduleManifestString.AppendLine(' PrivateData = @{')
        [void]$moduleManifestString.AppendLine('')
        [void]$moduleManifestString.AppendLine(' PSData = @{')
        [void]$moduleManifestString.AppendLine('')
        [void]$moduleManifestString.AppendLine(' # Tags applied to this module. These help with module discovery in online galleries.')
        [void]$moduleManifestString.AppendLine(" Tags = @('DSC', 'DesiredStateConfiguration', 'M365DSC', 'Microsoft365DSC', 'Microsoft365', 'CompositeResource')")
        [void]$moduleManifestString.AppendLine('')
        [void]$moduleManifestString.AppendLine(' # A URL to the license for this module.')
        [void]$moduleManifestString.AppendLine(" LicenseUri = 'https://github.com/ykuijs/M365DSC.CompositeResources/blob/main/LICENSE'")
        [void]$moduleManifestString.AppendLine('')
        [void]$moduleManifestString.AppendLine(' # A URL to the main website for this project.')
        [void]$moduleManifestString.AppendLine(" ProjectUri = 'https://github.com/ykuijs/M365DSC.CompositeResources'")
        [void]$moduleManifestString.AppendLine('')
        [void]$moduleManifestString.AppendLine(' # A URL to an icon representing this module.')
        [void]$moduleManifestString.AppendLine(" IconUri = 'https://github.com/microsoft/Microsoft365DSC/blob/Dev/Modules/Microsoft365DSC/Dependencies/Images/Logo.png?raw=true'")
        [void]$moduleManifestString.AppendLine('')
        [void]$moduleManifestString.AppendLine(' # ReleaseNotes of this module')
        [void]$moduleManifestString.AppendLine(" ReleaseNotes = 'Module belongs to Microsoft365DSC v$($m365dscVersion)'")
        [void]$moduleManifestString.AppendLine('')
        [void]$moduleManifestString.AppendLine(" ExternalModuleDependencies = @('Microsoft365DSC')")
        [void]$moduleManifestString.AppendLine('')
        [void]$moduleManifestString.AppendLine(' }')
        [void]$moduleManifestString.AppendLine(' }')
        [void]$moduleManifestString.AppendLine('}')

        $moduleManifestFileName = 'M365DSC.CompositeResources.psd1'
        $moduleManifestFilePath = Join-Path -Path $modulePath -ChildPath $moduleManifestFileName

        # Save the module manifest content to file
        Set-Content -Path $moduleManifestFilePath -Value $moduleManifestString.ToString()
    }
}
#EndRegion './Private/Initialize-Module.ps1' 134
#Region './Private/Invoke-TerminatingError.ps1' -1

function Invoke-TerminatingError
{
    <#
        .Synopsis
        Throws a terminating error.

        .Description
        This function throws a terminating error, which makes sure the code actually stops.

        .Example
        Invoke-TerminatingError

        .Parameter M
        The M parameter is the message that needs to be displayed when the error is thrown.
      #>

    [CmdletBinding()]
    [OutputType()]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.Object]
        $M
    )

    process
    {
        $PSCmdlet.ThrowTerminatingError((New-Object System.Management.Automation.ErrorRecord ([Exception]"$M"), $null, 0, $null))
    }
}
#EndRegion './Private/Invoke-TerminatingError.ps1' 30
#Region './Private/Save-Resource.ps1' -1

function Save-Resource
{
    <#
        .Synopsis
        Saves the composite resource content to file

        .Description
        This function saves the composite resource content to the
        schema file and creates the resource data file.

        .Parameter Config
        Specifies the content of the composite resource.

        .Parameter Workload
        Specifies the workload of the composite resource.

        .Parameter Version
        Specifies the version of the composite resource.

        .Parameter OutputPath
        Path to which files of the new composite resource are written.

        .Example
        Save-Resource -Config $content.ToString() -Workload 'Teams' -Version 1.23.1122.100 -OutputPath $OutputPath
    #>

    [CmdletBinding()]
    [OutputType()]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $Config,

        [Parameter(Mandatory = $true)]
        [System.String]
        $Workload,

        [Parameter(Mandatory = $true)]
        [System.String]
        $Version,

        [Parameter(Mandatory = $true)]
        [System.String]
        $OutputPath
    )

    process
    {
        $modulePath = Join-Path -Path $OutputPath -ChildPath "M365DSC.CompositeResources\$Version"
        $dscResourcesPath = Join-Path -Path $modulePath -ChildPath 'DscResources'

        # Save the schema file of the composite resource
        $resourceSavePath = Join-Path -Path $dscResourcesPath -ChildPath $Workload
        if ((Test-Path -Path $resourceSavePath) -eq $false)
        {
            $null = New-Item -Path $resourceSavePath -ItemType Directory
        }

        $schemaFileName = '{0}.schema.psm1' -f $Workload
        $schemaFilePath = Join-Path -Path $resourceSavePath -ChildPath $schemaFileName
        Set-Content -Path $schemaFilePath -Value $Config

        # Create the manifest content for the composite resource
        $manifestString = [System.Text.StringBuilder]::new()
        [void]$manifestString.AppendLine('@{')
        [void]$manifestString.AppendLine(" RootModule = '$Workload.schema.psm1'")
        [void]$manifestString.AppendLine('')
        [void]$manifestString.AppendLine(" ModuleVersion = '$Version'")
        [void]$manifestString.AppendLine('')
        [void]$manifestString.AppendLine(" GUID = '$(New-Guid)'")
        [void]$manifestString.AppendLine('')
        [void]$manifestString.AppendLine(" Author = 'Yorick Kuijs'")
        [void]$manifestString.AppendLine('')
        [void]$manifestString.AppendLine(" CompanyName = 'Microsoft'")
        [void]$manifestString.AppendLine('')
        [void]$manifestString.AppendLine(" Copyright = 'Copyright to Microsoft Corporation. All rights reserved.'")
        [void]$manifestString.AppendLine('')
        [void]$manifestString.AppendLine(" DscResourcesToExport = @('$Workload')")
        [void]$manifestString.AppendLine('}')

        $manifestFileName = '{0}.psd1' -f $Workload
        $schemaFilePath = Join-Path -Path $resourceSavePath -ChildPath $manifestFileName

        # Save the manifest content of the composite resource to file
        Set-Content -Path $schemaFilePath -Value $manifestString.ToString()
    }
}
#EndRegion './Private/Save-Resource.ps1' 88
#Region './Private/Write-Psd.ps1' -1

function Write-Psd
{
    <#
        .Synopsis
        Converts an object into a string so it can be written to PSD file.

        .Description
        This function converts an inputted object into a string so it can be written to a PSD1 file.

        .Example
        Write-Psd -Object $configData

        .Parameter Object
        Specifies the object that needs to be converted to a string.

        .Parameter Depth
        Specifies how deep the recursion should go. The default is 0, which means no recursion.

        .Parameter NoIndent
        Specifies that the output should not be indented.
    #>

    [CmdletBinding()]
    [OutputType()]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.Object]
        $Object,

        [Parameter()]
        [System.Int32]
        $Depth = 0,

        [Parameter()]
        [switch]
        $NoIndent
    )

    process
    {
        $indent1 = $script:Indent * $Depth
        if (!$NoIndent)
        {
            $script:Writer.Write($indent1)
        }

        if ($null -eq $Object)
        {
            $script:Writer.WriteLine('$null')
            return
        }

        $type = $Object.GetType()
        switch ([System.Type]::GetTypeCode($type))
        {
            Object
            {
                if ($type -eq [System.Guid] -or $type -eq [System.Version])
                {
                    $script:Writer.WriteLine("'{0}'", $Object)
                    return
                }
                if ($type -eq [System.Management.Automation.SwitchParameter])
                {
                    $script:Writer.WriteLine($(if ($Object)
                            {
                                '$true'
                            }
                            else
                            {
                                '$false'
                            }))
                    return
                }
                if ($type -eq [System.Uri])
                {
                    $script:Writer.WriteLine("'{0}'", $Object.ToString().Replace("'", "''"))
                    return
                }
                if ($script:Depth -and $Depth -ge $script:Depth)
                {
                    $script:Writer.WriteLine("''''")
                    ++$script:Pruned
                    return
                }
                if ($Object -is [System.Collections.IDictionary])
                {
                    if ($Object.Count)
                    {
                        $itemNo = 0
                        $script:Writer.WriteLine('@{')
                        $indent2 = $script:Indent * ($Depth + 1)
                        foreach ($e in $Object.GetEnumerator())
                        {
                            $key = $e.Key
                            $value = $e.Value
                            $keyType = $key.GetType()
                            if ($keyType -eq [string])
                            {
                                if ($key -match '^\w+$' -and $key -match '^\D')
                                {
                                    $script:Writer.Write('{0}{1} = ', $indent2, $key)
                                }
                                else
                                {
                                    $script:Writer.Write("{0}'{1}' = ", $indent2, $key.Replace("'", "''"))
                                }
                            }
                            elseif ($keyType -eq [int])
                            {
                                $script:Writer.Write('{0}{1} = ', $indent2, $key)
                            }
                            elseif ($keyType -eq [long])
                            {
                                $script:Writer.Write('{0}{1}L = ', $indent2, $key)
                            }
                            elseif ($script:Depth)
                            {
                                ++$script:Pruned
                                $script:Writer.Write('{0}item__{1} = ', $indent2, ++$itemNo)
                                $value = New-Object 'System.Collections.Generic.KeyValuePair[object, object]' $key, $value
                            }
                            else
                            {
                                throw "Not supported key type '$($keyType.FullName)'."
                            }
                            Write-Psd -Object $value -Depth ($Depth + 1) -NoIndent
                        }
                        $script:Writer.WriteLine("$indent1}")
                    }
                    else
                    {
                        $script:Writer.WriteLine('@{}')
                    }
                    return
                }
                if ($Object -is [System.Collections.IEnumerable])
                {
                    $script:Writer.Write('@(')
                    $empty = $true
                    foreach ($e in $Object)
                    {
                        if ($empty)
                        {
                            $empty = $false
                            $script:Writer.WriteLine()
                        }
                        Write-Psd -Object $e -Depth ($Depth + 1)
                    }
                    if ($empty)
                    {
                        $script:Writer.WriteLine(')')
                    }
                    else
                    {
                        $script:Writer.WriteLine("$indent1)" )
                    }
                    return
                }
                if ($Object -is [scriptblock])
                {
                    $script:Writer.WriteLine('{{{0}}}', $Object)
                    return
                }
                if ($Object -is [PSCustomObject] -or $script:Depth)
                {
                    $script:Writer.WriteLine('@{')
                    $indent2 = $script:Indent * ($Depth + 1)
                    foreach ($e in $Object.PSObject.Properties)
                    {
                        $key = $e.Name
                        if ($key -match '^\w+$' -and $key -match '^\D')
                        {
                            $script:Writer.Write('{0}{1} = ', $indent2, $key)
                        }
                        else
                        {
                            $script:Writer.Write("{0}'{1}' = ", $indent2, $key.Replace("'", "''"))
                        }
                        Write-Psd -Object $e.Value -Depth ($Depth + 1) -NoIndent
                    }
                    $script:Writer.WriteLine("$indent1}")
                    return
                }
            }
            String
            {
                $script:Writer.WriteLine("'{0}'", $Object.Replace("'", "''"))
                return
            }
            Boolean
            {
                $script:Writer.WriteLine($(if ($Object)
                        {
                            '$true'
                        }
                        else
                        {
                            '$false'
                        }))
                return
            }
            DateTime
            {
                $script:Writer.WriteLine("[DateTime] '{0}'", $Object.ToString('o'))
                return
            }
            Char
            {
                $script:Writer.WriteLine("'{0}'", $Object.Replace("'", "''"))
                return
            }
            DBNull
            {
                $script:Writer.WriteLine('$null')
                return
            }
            default
            {
                if ($type.IsEnum)
                {
                    $script:Writer.WriteLine("'{0}'", $Object)
                }
                else
                {
                    $script:Writer.WriteLine($Object)
                }
                return
            }
        }

        throw "Not supported type '{0}'." -f $type.FullName
    }
}
#EndRegion './Private/Write-Psd.ps1' 235
#Region './Public/New-CompositeResourceModule.ps1' -1

function New-CompositeResourceModule
{
    <#
        .Synopsis
        Generate a new module with composite resources based on Microsoft365DSC.

        .Description
        This function generates a new module with composite resources for Microsoft365DSC, split into workloads.

        .Example
        New-CompositeResourceModule -OutputPath 'C:\Temp' -Version '11.23.1122.100'

        .Parameter OutputPath
        Specifies the path in which the new module should be generated.

        .Parameter Version
        Specifies the version of the new module should be generated. This should be related to the version of Microsoft365DSC.
        E.g. Version is 1.23.1122.100 when the Microsoft365DSC version is 1.23.1122.1
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "", Justification="Using Write-Host to format output")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Justification="Not changing state")]
    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param (
        [Parameter(Mandatory = $true)]
        [System.String]
        $OutputPath,

        [Parameter(Mandatory = $true)]
        [System.String]
        $Version
    )

    process
    {
        # These properties should be ignored by the code because they are not used in the composite resource
        $ignoredProperties = @('Credential', 'CertificatePassword', 'CertificatePath', 'ApplicationSecret', 'ManagedIdentity', 'DependsOn', 'PsDscRunAsCredential')

        $prerequisitesDatafile = Join-Path -Path $PSScriptRoot -ChildPath 'Dependencies.psd1'

        if (Test-Path -Path $prerequisitesDatafile)
        {
            $resourceDependencies = Import-PowerShellDataFile -Path $prerequisitesDatafile
        }
        else
        {
            Write-Host -Object "[ERROR] Prerequisites data file '$prerequisitesDatafile' not found!" -ForegroundColor Red
            return $false
        }

        $script:errors = @()

        $configData = [Ordered]@{
            AllNodes    = @(
                @{
                    NodeName        = 'String | Required | Name of the host of which the LCM is used, normally this is localhost'
                    CertificateFile = 'String | Required | Relative path to the public key of the DSC credential encryption certificate, e.g. .\DSCCertificate.cer'
                }
            )
            NonNodeData = [Ordered]@{
                Environment    = @{
                    Name             = 'String | Required | Name of your environment, e.g. TestEnvironment'
                    ShortName        = 'String | Required | Abbreviation of the environment name, e.g. TST'
                    TenantId         = 'String | Required | Tenant URL, e.g. test.onmicrosoft.com'
                    OrganizationName = 'String | Required | Name of your organization, prefix of the tenant id, e.g. test'
                    CICD             = @{
                        DependsOn     = 'String | Required | Name of the environment this environment depends on, e.g. TestEnvironment'
                        UseCodeBranch = 'String | Required | Name of the branch that is used for the CICD (Script) repository, e.g. main'
                        Approvers = @(
                            @{
                                Principal = 'String | Required | Principal of the user or groups that needs to get added to the approvers list.'
                                Type      = 'String | Required | Type of principal | User / Group'
                            }
                        )
                    }
                    UsedWorkloads = @{}
                    Tokens = @{
                        ExampleToken = 'String | Optional | Example of a token that can be used anywhere in the config, by specifying {{ExampleToken}}'
                    }
                }
                AppCredentials = @(
                    @{
                        Workload       = 'String | Required | Name of the Workload for which this credential will be used | AzureAD / Exchange / Intune / Office365 / OneDrive / Planner / PowerPlatform / SecurityCompliance / SharePoint / Teams'
                        ApplicationId  = 'Guid | Required | The GUID of the Entra ID Service Principal'
                        CertThumbprint = 'String | Required | The Certificate Thumbprint of the certificate used for authentication'
                    }
                )
            }
        }
        #endregion Initialize Variables

        #region Main script
        # Get the Microsoft365DSC module
        $m365dscVersion = $Version.Substring(0, $Version.LastIndexOf('.') + 2)
        [Array]$m365Module = Get-Module Microsoft365DSC -ListAvailable | Where-Object -FilterScript {
            $_.Version -eq $m365dscVersion
        }

        # Check if the module has been retrieved or not
        if ($m365Module.Count -gt 0)
        {
            if ($m365Module.Count -gt 1)
            {
                Write-Host -Object 'Found multiple versions of Microsoft365DSC. Using the latest version.'
                $m365Module = $m365Module | Select-Object -First 1
            }

            # Initialize the module
            Initialize-Module -Version $Version -OutputPath $OutputPath

            # Get the module root path and find all schema.mof files in the module
            $m365modulePath = Split-Path -Path $m365Module.Path -Parent
            $mofSearchPath = Join-Path -Path $m365modulePath -ChildPath '\**\**.schema.mof'
            $mofSchemaFiles = @(Get-ChildItem -Path $mofSearchPath -Recurse)

            Write-Host -Object ("Found {0} MOF files in path '{1}'." -f $mofSchemaFiles.Count, $m365modulePath) -ForegroundColor Cyan

            if ($mofSchemaFiles.Count -eq 0)
            {
                return $false
            }

            Write-Host -Object ' '
            Write-Host -Object 'Processing resources:' -ForegroundColor Cyan

            $lastWorkload = ''

            # Loop through all the Schema files found in the modules folder
            :schemaloop foreach ($mofSchemaFile in $mofSchemaFiles)
            {
                # Read schema
                $mofSchemas = Get-MofSchemaObject -FileName $mofSchemaFile.FullName
                $dscResourceName = $mofSchemaFile.Name.Replace('.schema.mof', '')
                $shortResourceName = $dscResourceName -replace '^MSFT_'

                Write-Host -Object " - $shortResourceName" -ForegroundColor Green

                # Geth the main CIM class of the resource
                $resourceSchema = $mofSchemas |
                    Where-Object -FilterScript {
                            ($_.ClassName -eq $dscResourceName) -and ($null -ne $_.FriendlyName)
                    }

                # Determine which workload the current resource is in
                switch ($shortResourceName)
                {
                    { $_.StartsWith('AAD') }
                    {
                        $resourceWorkload = 'AzureAD'
                        $customResourceName = $shortResourceName -replace "^AAD"
                    }
                    { $_.StartsWith('EXO') }
                    {
                        $resourceWorkload = 'Exchange'
                        $customResourceName = $shortResourceName -replace '^EXO'
                    }
                    { $_.StartsWith('Intune') }
                    {
                        $resourceWorkload = 'Intune'
                        $customResourceName = $shortResourceName -replace "^$resourceWorkload"
                    }
                    { $_.StartsWith('O365') }
                    {
                        $resourceWorkload = 'Office365'
                        $customResourceName = $shortResourceName -replace '^O365'
                    }
                    { $_.StartsWith('OD') }
                    {
                        $resourceWorkload = 'OneDrive'
                        $customResourceName = $shortResourceName -replace '^OD'
                    }
                    { $_.StartsWith('Planner') }
                    {
                        $resourceWorkload = 'Planner'
                        $customResourceName = $shortResourceName -replace "^$resourceWorkload"
                    }
                    { $_.StartsWith('PP') }
                    {
                        $resourceWorkload = 'PowerPlatform'
                        $customResourceName = $shortResourceName -replace '^PP'
                    }
                    { $_.StartsWith('SC') }
                    {
                        $resourceWorkload = 'SecurityCompliance'
                        $customResourceName = $shortResourceName -replace '^SC'
                    }
                    { $_.StartsWith('SPO') }
                    {
                        $resourceWorkload = 'SharePoint'
                        $customResourceName = $shortResourceName -replace '^SPO'
                    }
                    { $_.StartsWith('Teams') }
                    {
                        $resourceWorkload = 'Teams'
                        $customResourceName = $shortResourceName -replace "^$resourceWorkload"
                    }
                    { $_.StartsWith('M365DSC') }
                    {
                        Write-Host ' Skipping M365DSC workload (RuleEvaluation)' -ForegroundColor 'Yellow'
                        continue schemaloop
                    }
                    Default
                    {
                        Write-Error 'Unknown workload for this resource!'
                        $script:errors += [PSCustomObject]@{
                            Type = 'Error'
                            Message = "Unknown workload for resource '$shortResourceName'"
                        }
                        continue
                    }
                }

                # Check if the last workload matches the current workload
                if ($lastWorkload -ne $resourceWorkload)
                {
                    # Last workload is different from the current workload.
                    # Check if this is the first workload or a subsequent workload
                    if ([String]::IsNullOrEmpty($lastWorkload) -eq $false)
                    {
                        # Is a subsequent workload, so wrap up previous workload and save resource
                        [void]$configString.Append('}')
                        Save-Resource -Config $configString.ToString() -Workload $lastWorkload -Version $Version -OutputPath $OutputPath
                    }

                    $lastWorkload = $resourceWorkload

                    if ($null -eq $configData.NonNodeData.$resourceWorkload)
                    {
                        $configData.NonNodeData.$resourceWorkload = [Ordered]@{}
                    }

                    $configData.NonNodeData.Environment.UsedWorkloads.$resourceWorkload = $true

                    # Initialize new composite resource content
                    $configString = [System.Text.StringBuilder]::new()
                    [void]$configString.AppendLine("# ($(Get-Date -f 'yyyy-MM-dd HH:mm:ss')) Generated using Microsoft365DSC v$($m365Module.Version)")
                    [void]$configString.AppendLine("Configuration '$($resourceWorkload)'")
                    [void]$configString.AppendLine('{')
                    [void]$configString.AppendLine(' param')
                    [void]$configString.AppendLine(' (')
                    [void]$configString.AppendLine(' [Parameter(Mandatory = $true)]')
                    [void]$configString.AppendLine(' [System.String]')
                    [void]$configString.AppendLine(' $ApplicationId,')
                    [void]$configString.AppendLine('')
                    [void]$configString.AppendLine(' [Parameter(Mandatory = $true)]')
                    [void]$configString.AppendLine(' [System.String]')
                    [void]$configString.AppendLine(' $TenantId,')
                    [void]$configString.AppendLine('')
                    [void]$configString.AppendLine(' [Parameter(Mandatory = $true)]')
                    [void]$configString.AppendLine(' [System.String]')
                    [void]$configString.AppendLine(' $CertificateThumbprint')
                    [void]$configString.AppendLine(' )')
                    [void]$configString.AppendLine('')
                    [void]$configString.AppendLine(' Import-DscResource -ModuleName Microsoft365DSC')
                }

                $identityGlobalResource = $false
                $indent = 1

                # Generate Configuration Data location of the settings
                $configDataLocation = '$ConfigurationData.NonNodeData.{0}.{1}' -f $lastWorkload, $customResourceName

                # Checking for missing CertificateThumbprint parameter and skip resource if it is (TeamsUserCallingSettings resource)
                if (($resourceSchema.Attributes | Where-Object -FilterScript { $_.Name -eq 'CertificateThumbprint' }) -isnot [System.Collections.Hashtable])
                {
                    Write-Host ' Resource does not support CertificateThumbprint authentication. Skipping resource for now!' -ForegroundColor Red
                    $script:errors += [PSCustomObject]@{
                        Type = 'Warning'
                        Message = "Resource '$shortResourceName' does not support CertificateThumbprint authentication. Resource has been skipped!"
                    }
                    continue
                }

                # Check if the current DSC resource is a SingleInstance resource
                if (($resourceSchema.Attributes | Where-Object -FilterScript { $_.Name -eq 'IsSingleInstance' }) -is [System.Collections.Hashtable])
                {
                    # IsSingleInstance resources can only exist once and therefore should not loop through an array
                    Write-Debug -Message ' * Has IsSingleInstance'

                    $configData.NonNodeData.$resourceWorkload.$customResourceName = [Ordered]@{}
                    $currentDataObject = $configData.NonNodeData.$resourceWorkload.$customResourceName

                    # Single instance resources should
                    [void]$configString.AppendLine('')

                    [void]$configString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $indent), "if (`$ConfigurationData.NonNodeData.$resourceWorkload.ContainsKey('$customResourceName'))"))
                    [void]$configString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $indent), '{'))

                    $indent++
                    $resourceTitle = "$($customResourceName)Defaults"

                    [void]$configString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $indent), "`$resourceTitle = '$resourceTitle'"))
                    [void]$configString.AppendLine('')

                    # Adding parameters hashtable initialization to the code
                    [void]$configString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $indent), "`$parameters = $configDataLocation"))
                    [void]$configString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $indent), "`$parameters.IsSingleInstance = 'Yes'"))
                }
                # Check if the current DSC resource has an Identity parameter which only accepts the value 'Global'
                elseif (($resourceSchema.Attributes | Where-Object -FilterScript { $_.Name -eq 'Identity' -and $_.ValueMap -eq 'Global' }) -is [System.Collections.Hashtable])
                {
                    # Global resources can only exist once and therefore should not loop through an array
                    $identityGlobalResource = $true
                    Write-Debug -Message ' * Identity = Global'

                    $configData.NonNodeData.$resourceWorkload.$customResourceName = @{}
                    $currentDataObject = $configData.NonNodeData.$resourceWorkload.$customResourceName

                    [void]$configString.AppendLine('')

                    [void]$configString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $indent), "if (`$ConfigurationData.NonNodeData.$resourceWorkload.ContainsKey('$customResourceName'))"))
                    [void]$configString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $indent), '{'))

                    $indent++
                    $resourceTitle = "$($customResourceName)Defaults"

                    [void]$configString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $indent), "`$resourceTitle = '$resourceTitle'"))
                    [void]$configString.AppendLine('')

                    # Adding parameters hashtable initialization to the code
                    [void]$configString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $indent), "`$parameters = $configDataLocation"))
                    [void]$configString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $indent), "`$parameters.Identity = 'Global'"))
                }
                # All other resources should be processed similar
                else
                {
                    # All other resources can exist multiple times and therefore should loop through an array
                    [void]$configString.AppendLine('')

                    # Generate the plural name of the data node
                    if ($shortResourceName.EndsWith('y'))
                    {
                        $dataName = $customResourceName -replace 'y$', 'ies'
                    }
                    elseif ($shortResourceName -like '*Policy*')
                    {
                        $dataName = $customResourceName -replace 'Policy', 'Policies'
                    }
                    elseif ($shortResourceName -like '*Profile*')
                    {
                        $dataName = $customResourceName -replace 'Profile', 'Profiles'
                    }
                    elseif ($shortResourceName.EndsWith('Settings'))
                    {
                        $dataName = $customResourceName += 'Items'
                    }
                    else
                    {
                        $dataName = $customResourceName + 's'
                    }

                    $configData.NonNodeData.$resourceWorkload.$dataName = @(@{})
                    $currentDataObject = $configData.NonNodeData.$resourceWorkload.$dataName[0]

                    # Add foreach to loop through the array
                    [void]$configString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $indent), "foreach (`$$($customResourceName) in `$ConfigurationData.NonNodeData.$($lastWorkload).$($dataName))"))
                    [void]$configString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $indent), '{'))
                    $indent++

                    # Shorten the Configuration Data location
                    $configDataLocation = '${0}' -f $customResourceName

                    # Generate name of the resource, using the Key parameter(s)
                    $mandatoryProperties = $resourceSchema.Attributes | Where-Object { $_.State -in @('Key') }
                    if ($mandatoryProperties.Name -is [System.Array])
                    {
                        # Multiple key parameters found, combine these names
                        $paramFullPlaceholder = ''
                        $paramPlaceholder = ''
                        for ($i = 0; $i -lt $mandatoryProperties.Name.Count; $i++)
                        {
                            $paramFullPlaceholder += "$configDataLocation.{$i},"
                            $paramPlaceholder += "{$i}-"
                        }
                        $paramFullPlaceholder = $paramFullPlaceholder.TrimEnd(',')
                        $paramPlaceholder = $paramPlaceholder.TrimEnd('-')

                        $resourceTitle = '$resourceTitle = ''{0}-[parameter]'' -f {1}' -f $shortResourceName, $paramFullPlaceholder
                        $resourceTitle = $resourceTitle -f $mandatoryProperties.Name
                        $resourceTitle = $resourceTitle -replace '\[parameter\]', $paramPlaceholder
                    }
                    else
                    {
                        # Single key parameter found
                        $resourceTitle = '$resourceTitle = ''{0}-{1}'' -f {2}.{3}' -f $shortResourceName, '{0}', $configDataLocation, $mandatoryProperties.Name
                    }

                    # Adding resource title generation
                    [void]$configString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $indent), $resourceTitle))
                    [void]$configString.AppendLine('')

                    # Adding parameters hashtable initialization to the code
                    [void]$configString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $indent), "`$parameters = $configDataLocation"))
                }

                [void]$configString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $indent), "`$parameters.ApplicationId = `$ApplicationId"))
                [void]$configString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $indent), "`$parameters.TenantId = `$TenantId"))
                [void]$configString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $indent), "`$parameters.CertificateThumbprint = `$CertificateThumbprint"))
                [void]$configString.AppendLine('')

                [void]$configString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $indent), "if (`$parameters.ContainsKey('UniqueId'))"))
                [void]$configString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $indent), '{'))
                [void]$configString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $indent), " `$parameters.Remove('UniqueId')"))
                [void]$configString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $indent), '}'))

                $script:currentDepth = 0
                $script:maxDepth = 8

                # Process all parameters, but handle embedded parameters separately
                $result = Get-EmbeddedPropertyString -Properties $resourceSchema.Attributes -Indentation $indent -ParameterName '$parameters'
                if ([String]::IsNullOrEmpty($result) -eq $false)
                {
                    [void]$configString.Append($result)
                }

                # Add DependsOn parameter, when necessary.
                if ($resourceDependencies.ContainsKey($shortResourceName))
                {
                    $dependsString = $resourceDependencies.$shortResourceName

                    [void]$configString.AppendLine(("{0}{1} = {2}" -f (Get-IndentationString -Indentation $indent), '$parameters.DependsOn', $dependsString))
                    [void]$configString.AppendLine('')
                }

                [void]$configString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $indent), "(Get-DscSplattedResource -ResourceName '$shortResourceName' -ExecutionName `$resourceTitle -Properties `$parameters -NoInvoke).Invoke(`$parameters)"))

                # Get all the properties in the resource and filter out those that should be ignored
                $filteredProperties = $resourceSchema.Attributes | Where-Object { $_.Name -notin $ignoredProperties } | Sort-Object -Property Name

                $script:currentDepth = 0

                # Check if the resource needs a UniqueId parameter by checking if the names of the properties contain
                # any of the below property names. If they don't, a UniqueId is required.
                $propertiesNeedUniqueId = @("Id", "Identity", "IsSingleInstance")
                $diff = Compare-Object -ReferenceObject $filteredProperties.Name -DifferenceObject $propertiesNeedUniqueId -ExcludeDifferent -IncludeEqual
                if ($null -eq $diff)
                {
                    $currentDataObject.UniqueId = ('{0} | {1} | {2}' -f "String", "Required", "Unique ID to identify this specific object")
                }

                # Loop through all the filtered properties and build example config data file
                foreach ($property in $filteredProperties)
                {
                    # Check if the property is an EmbeddedInstance
                    if ($null -ne $property.EmbeddedInstance -and $property.EmbeddedInstance -ne 'MSFT_Credential')
                    {
                        $null = Get-AttributeString -Property $property -ConfigData $currentDataObject
                    }
                    else
                    {
                        if ($property.Name -notin @('IsSingleInstance','ApplicationId', 'CertificateThumbprint', 'TenantId') -and
                            (($identityGlobalResource -eq $false) -or ($identityGlobalResource -and $property.Name -ne 'Identity')))
                        {
                            # For all other parameter, generate the correct parameter value name from the Configuration Data
                            $propertyDataType = $property.DataType

                            if ($propertyDataType -eq 'Instance' -and $property.EmbeddedInstance -eq 'MSFT_Credential')
                            {
                                $propertyDataType = 'PSCredential'
                            }

                            $state = 'Optional'
                            if ($property.State -in @('Key', 'Required'))
                            {
                                $state = 'Required'
                            }

                            if ($null -eq $property.ValueMap)
                            {
                                if ($propertyDataType -like "*Array")
                                {
                                    $propertyDataType = $propertyDataType -replace "Array"
                                    $result = @(('{0} | {1} | {2}' -f $propertyDataType, $state, $property.Description))
                                }
                                else
                                {
                                    $result = ('{0} | {1} | {2}' -f $propertyDataType, $state, $property.Description)
                                }
                            }
                            else
                            {
                                if ($propertyDataType -like "*Array")
                                {
                                    $propertyDataType = $propertyDataType -replace "Array"
                                    $result = @(('{0} | {1} | {2} | {3}' -f $propertyDataType, $state, $property.Description, ($property.ValueMap -join ' / ')))
                                }
                                else
                                {
                                    $result = ('{0} | {1} | {2} | {3}' -f $propertyDataType, $state, $property.Description, ($property.ValueMap -join ' / '))
                                }
                            }
                            $currentDataObject.$($property.Name) = $result
                        }
                    }
                }

                $indent--
                [void]$configString.AppendLine(('{0}{1}' -f (Get-IndentationString -Indentation $indent), '}'))
            }

            # Last workload is processed. Make sure the this resource is also saved to file
            [void]$configString.Append('}')
            Save-Resource -Config $configString.ToString() -Workload $lastWorkload -Version $Version -OutputPath $OutputPath

            Write-Host -Object 'Writing ConfigurationData file' -ForegroundColor Cyan
            $psdStringData = "# ($(Get-Date -f 'yyyy-MM-dd HH:mm:ss')) Generated using Microsoft365DSC v$($m365Module.Version)`n"
            $psdStringData += $configData | ConvertTo-Psd
            $psdPath = Join-Path -Path $OutputPath -ChildPath "M365DSC.CompositeResources\$Version\M365ConfigurationDataExample.psd1"
            Set-Content -Path $psdPath -Value $psdStringData

            Write-Host -Object 'Encountered issues:' -ForegroundColor Cyan
            foreach ($err in $script:errors)
            {
                Write-Host -Object ("{0,-10} | {1}"-f $err.Type,$err.Message)
            }

            return $true
        }
        else
        {
            # Can't generate the module, since the Microsoft365DSC module cannot be found.
            Write-Host -Object 'Microsoft365DSC not found!' -ForegroundColor Red
            return $false
        }
        #endregion

    }
}
#EndRegion './Public/New-CompositeResourceModule.ps1' 529
#Region './suffix.ps1' -1

# Inspired from https://github.com/nightroman/Invoke-Build/blob/64f3434e1daa806814852049771f4b7d3ec4d3a3/Tasks/Import/README.md#example-2-import-from-a-module-with-tasks
Get-ChildItem -Path (Join-Path -Path $PSScriptRoot -ChildPath 'tasks\*') -Include '*.build.*' |
    ForEach-Object -Process {
        $ModuleName = ([System.IO.FileInfo] $MyInvocation.MyCommand.Name).BaseName
        $taskFileAliasName = "$($_.BaseName).$ModuleName.ib.tasks"
        Set-Alias -Name $taskFileAliasName -Value $_.FullName

        Export-ModuleMember -Alias $taskFileAliasName
    }
#EndRegion './suffix.ps1' 10