DSCResources/MSFT_AADServicePrincipal/MSFT_AADServicePrincipal.psm1

Confirm-M365DSCModuleDependency -ModuleName 'MSFT_AADServicePrincipal'
$Script:PropertiesToExport = 'AppDisplayName', 'AppId', 'Id', 'DisplayName', 'CustomSecurityAttributes', 'AlternativeNames', 'AccountEnabled', 'AppRoleAssignmentRequired', 'ErrorUrl', 'Homepage', 'LogoutUrl', 'Notes', 'PreferredSingleSignOnMode', 'PublisherName', 'ReplyUrls', 'SamlMetadataURL', 'ServicePrincipalNames', 'ServicePrincipalType', 'Tags', 'KeyCredentials', 'PasswordCredentials'

function Get-TargetResource
{
    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable])]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $AppId,

        [Parameter()]
        [Microsoft.Management.Infrastructure.CimInstance[]]
        $AppRoleAssignedTo,

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

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

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

        [Parameter()]
        [System.Boolean]
        $AccountEnabled,

        [Parameter()]
        [System.Boolean]
        $AppRoleAssignmentRequired,

        [Parameter()]
        [Microsoft.Management.Infrastructure.CimInstance]
        $ClaimsPolicy,

        [Parameter()]
        [Microsoft.Management.Infrastructure.CimInstance[]]
        $CustomSecurityAttributes,

        [Parameter()]
        [Microsoft.Management.Infrastructure.CimInstance[]]
        $DelegatedPermissionClassifications,

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

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

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

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

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

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

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

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

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

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

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

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

        [Parameter()]
        [Microsoft.Management.Infrastructure.CimInstance[]]
        $KeyCredentials,

        [Parameter()]
        [Microsoft.Management.Infrastructure.CimInstance[]]
        $PasswordCredentials,

        [Parameter()]
        [ValidateSet('Present', 'Absent')]
        [System.String]
        $Ensure = 'Present',

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

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

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

        [Parameter()]
        [System.Management.Automation.PSCredential]
        $ApplicationSecret,

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

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

        [Parameter()]
        [System.Management.Automation.PSCredential]
        $CertificatePassword,

        [Parameter()]
        [Switch]
        $ManagedIdentity,

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

    Write-Verbose -Message "Getting configuration of AAD Service Principal with AppId {$AppId}"

    try
    {
        if (-not $Script:exportedInstance -or $Script:exportedInstance.AppId -ne $AppId)
        {
            $null = New-M365DSCConnection -Workload 'MicrosoftGraph' `
                -InboundParameters $PSBoundParameters

            #Ensure the proper dependencies are installed in the current environment.
            Confirm-M365DSCDependencies

            #region Telemetry
            $ResourceName = $MyInvocation.MyCommand.ModuleName -replace 'MSFT_', ''
            $CommandName = $MyInvocation.MyCommand
            $data = Format-M365DSCTelemetryParameters -ResourceName $ResourceName `
                -CommandName $CommandName `
                -Parameters $PSBoundParameters
            Add-M365DSCTelemetryEvent -Data $data
            #endregion

            $nullReturn = $PSBoundParameters
            $nullReturn.Ensure = 'Absent'

            if (-not [System.String]::IsNullOrEmpty($ObjectID))
            {
                $AADServicePrincipal = Get-MgServicePrincipal -ServicePrincipalId $ObjectId `
                    -Property $Script:PropertiesToExport `
                    -ExpandProperty 'AppRoleAssignedTo' `
                    -ErrorAction SilentlyContinue
            }

            if ($null -eq $AADServicePrincipal)
            {
                if (-not [System.Guid]::TryParse($AppId, [ref][System.Guid]::Empty))
                {
                    $AADServicePrincipal = [Array](Get-MgServicePrincipal -Filter "DisplayName eq '$($AppId -replace "'", "''")'" `
                            -Property $Script:PropertiesToExport `
                            -Expand 'AppRoleAssignedTo')
                    if ($null -ne $AADServicePrincipal -and $AADServicePrincipal.Count -gt 1)
                    {
                        throw "Multiple Service Principal with the DisplayName $($AppId) exist in the tenant."
                    }
                }
                else
                {
                    $AADServicePrincipal = Get-MgServicePrincipal -Filter "AppID eq '$($AppId)'" `
                        -Property $Script:PropertiesToExport `
                        -Expand 'AppRoleAssignedTo'
                }
            }
            if ($null -eq $AADServicePrincipal)
            {
                Write-Verbose -Message "Service Principal with AppId '$AppId' not found."
                return $nullReturn
            }
        }
        else
        {
            $AADServicePrincipal = $Script:exportedInstance
        }

        $batchRequests = @(
            @{
                id     = 'AppRoleAssignedTo'
                method = 'GET'
                url    = "/servicePrincipals/$($AADServicePrincipal.Id)/appRoleAssignedTo"
            }
            @{
                id     = 'Owners'
                method = 'GET'
                url    = "/servicePrincipals/$($AADServicePrincipal.Id)/owners"
            }
            @{
                id     = 'delegatedPermissionClassifications'
                method = 'GET'
                url    = "/servicePrincipals/$($AADServicePrincipal.Id)/delegatedPermissionClassifications"
            }
            @{
                id     = 'claimsPolicy'
                method = 'GET'
                url    = "/servicePrincipals/$($AADServicePrincipal.Id)/claimsPolicy"
            }
        )
        $batchResponse = Invoke-M365DSCGraphBatchRequest -Requests $batchRequests -ErrorAction SilentlyContinue

        $AppRoleAssignedToValues = @()
        $assignmentsValue = ($batchResponse | Where-Object -FilterScript { $_.id -eq 'AppRoleAssignedTo' }).body.value
        foreach ($principal in $assignmentsValue)
        {
            $currentAssignment = @{
                PrincipalType = $null
                Identity      = $null
            }
            if ($principal.PrincipalType -eq 'User')
            {
                $user = Get-MgUser -UserId $principal.PrincipalId
                $currentAssignment.PrincipalType = 'User'
                $currentAssignment.Identity = $user.UserPrincipalName
                $AppRoleAssignedToValues += $currentAssignment
            }
            elseif ($principal.PrincipalType -eq 'Group')
            {
                $group = Get-MgGroup -GroupId $principal.PrincipalId
                $currentAssignment.PrincipalType = 'Group'
                $currentAssignment.Identity = $group.DisplayName
                $AppRoleAssignedToValues += $currentAssignment
            }
        }

        $ownersValues = @()
        $ownersInfo = ($batchResponse | Where-Object -FilterScript { $_.id -eq 'Owners' }).body.value
        foreach ($ownerInfo in $ownersInfo)
        {
            if ($ownerInfo.'@odata.type' -eq '#microsoft.graph.user')
            {
                $ownersValues += $ownerInfo.UserPrincipalName
            }
            else
            {
                $ownersValues += $ownerInfo.DisplayName
            }
        }

        $claimsPolicyValue = $null
        $claimsPolicyResponse = ($batchResponse | Where-Object -FilterScript { $_.id -eq 'claimsPolicy' })
        if ($claimsPolicyResponse -and $claimsPolicyResponse.status -eq 200 -and $claimsPolicyResponse.body)
        {
            $claimsPolicyValue = Convert-M365DSCDRGComplexTypeToHashtable -ComplexObject $claimsPolicyResponse.body
            $claimsPolicyValue.Remove('@odata.context') | Out-Null
            $claimsPolicyValue.Remove('id') | Out-Null
        }

        #Managed Identities in AzureGov return exception when pulling delegatedPermissionClassifications
        [Array]$complexDelegatedPermissionClassifications = @()
        try
        {
            $permissionClassifications = ($batchResponse | Where-Object -FilterScript { $_.id -eq 'delegatedPermissionClassifications' }).body.value
        }
        catch
        {
            Write-Verbose -Message "Service Principal didn't return delegated permission classifications. Expected for Managed Identities."
        }

        foreach ($permissionClassification in $permissionClassifications.Value)
        {
            $hashtable = @{
                classification = $permissionClassification.Classification
                permissionName = $permissionClassification.permissionName
            }
            $complexDelegatedPermissionClassifications += $hashtable
        }

        $complexKeyCredentials = @()
        foreach ($currentkeyCredentials in $AADServicePrincipal.keyCredentials)
        {
            $mykeyCredentials = [ordered]@{}
            if ($null -ne $currentkeyCredentials.customKeyIdentifier)
            {
                $mykeyCredentials.Add('CustomKeyIdentifier', $currentkeyCredentials.customKeyIdentifier)
            }
            $mykeyCredentials.Add('DisplayName', $currentkeyCredentials.displayName)
            if ($null -ne $currentkeyCredentials.endDateTime)
            {
                $mykeyCredentials.Add('EndDateTime', ([DateTimeOffset]$currentkeyCredentials.endDateTime).ToString('o'))
            }
            $mykeyCredentials.Add('KeyId', $currentkeyCredentials.keyId)

            if ($null -ne $currentkeyCredentials.Key)
            {
                $mykeyCredentials.Add('Key', $currentkeyCredentials.Key)
            }

            if ($null -ne $currentkeyCredentials.startDateTime)
            {
                $mykeyCredentials.Add('StartDateTime', ([DateTimeOffset]$currentkeyCredentials.startDateTime).ToString('o'))
            }
            $mykeyCredentials.Add('Type', $currentkeyCredentials.type)
            $mykeyCredentials.Add('Usage', $currentkeyCredentials.usage)
            if ($mykeyCredentials.values.Where({ $null -ne $_ }).Count -gt 0)
            {
                $complexKeyCredentials += $mykeyCredentials
            }
        }

        $complexPasswordCredentials = @()
        foreach ($currentpasswordCredentials in $AADServicePrincipal.passwordCredentials)
        {
            $mypasswordCredentials = [ordered]@{}
            $mypasswordCredentials.Add('DisplayName', $currentpasswordCredentials.displayName)
            if ($null -ne $currentpasswordCredentials.endDateTime)
            {
                $mypasswordCredentials.Add('EndDateTime', ([DateTimeOffset]$currentpasswordCredentials.endDateTime).ToString('o'))
            }
            $mypasswordCredentials.Add('Hint', $currentpasswordCredentials.hint)
            $mypasswordCredentials.Add('KeyId', $currentpasswordCredentials.keyId)
            if ($null -ne $currentpasswordCredentials.startDateTime)
            {
                $mypasswordCredentials.Add('StartDateTime', ([DateTimeOffset]$currentpasswordCredentials.startDateTime).ToString('o'))
            }
            if ($mypasswordCredentials.values.Where({ $null -ne $_ }).Count -gt 0)
            {
                $complexPasswordCredentials += $mypasswordCredentials
            }
        }

        $complexCustomSecurityAttributes = [Array](Get-CustomSecurityAttributes -ServicePrincipal $AADServicePrincipal)
        if ($null -eq $complexCustomSecurityAttributes)
        {
            $complexCustomSecurityAttributes = @()
        }

        # If the App Id was passed in as a Guid, return it as a GUID. Otherwise return it as text.
        if (-not [System.String]::IsNullOrEmpty($AppId) -and [System.Guid]::TryParse($AppId, [ref][System.Guid]::Empty))
        {
            Write-Verbose -Message 'Returning AppId as GUID since the provided value was in GUID format.'
            $appIdToExport = $AADServicePrincipal.AppId
        }
        else
        {
            Write-Verbose -Message 'Returning AppId as Display Name since the provided value was NOT in GUID format.'
            $appIdToExport = $AADServicePrincipal.DisplayName
        }

        $tagsValue = @()
        if ($null -ne $AADServicePrincipal.Tags)
        {
            $tagsValue = [Array]($AADServicePrincipal.Tags)
        }

        $alternativeNamesValue = @()
        if ($null -ne $AADServicePrincipal.AlternativeNames)
        {
            $alternativeNamesValue = [Array]($AADServicePrincipal.AlternativeNames)
        }

        $replyUrlsValue = @()
        if ($null -ne $AADServicePrincipal.ReplyURLs)
        {
            $replyUrlsValue = [Array]($AADServicePrincipal.ReplyURLs)
        }

        $servicePrincipalNamesValue = @()
        if ($null -ne $AADServicePrincipal.ServicePrincipalNames)
        {
            $servicePrincipalNamesValue = [Array]($AADServicePrincipal.ServicePrincipalNames)
        }

        $result = @{
            AppId                              = $appIdToExport
            AppRoleAssignedTo                  = $AppRoleAssignedToValues
            ObjectID                           = $AADServicePrincipal.Id
            DisplayName                        = $AADServicePrincipal.DisplayName
            AlternativeNames                   = $alternativeNamesValue
            AccountEnabled                     = [boolean]$AADServicePrincipal.AccountEnabled
            AppRoleAssignmentRequired          = $AADServicePrincipal.AppRoleAssignmentRequired
            ClaimsPolicy                       = $claimsPolicyValue
            CustomSecurityAttributes           = $complexCustomSecurityAttributes
            DelegatedPermissionClassifications = [Array]$complexDelegatedPermissionClassifications
            ErrorUrl                           = $AADServicePrincipal.ErrorUrl
            Homepage                           = $AADServicePrincipal.Homepage
            LogoutUrl                          = $AADServicePrincipal.LogoutUrl
            Notes                              = $AADServicePrincipal.Notes
            Owners                             = $ownersValues
            PreferredSingleSignOnMode          = $AADServicePrincipal.PreferredSingleSignOnMode
            PublisherName                      = $AADServicePrincipal.PublisherName
            ReplyURLs                          = $replyUrlsValue
            SamlMetadataURL                    = $AADServicePrincipal.SamlMetadataURL
            ServicePrincipalNames              = $servicePrincipalNamesValue
            ServicePrincipalType               = $AADServicePrincipal.ServicePrincipalType
            Tags                               = $tagsValue
            KeyCredentials                     = $complexKeyCredentials
            PasswordCredentials                = $complexPasswordCredentials
            Ensure                             = 'Present'
            Credential                         = $Credential
            ApplicationId                      = $ApplicationId
            ApplicationSecret                  = $ApplicationSecret
            TenantId                           = $TenantId
            CertificateThumbprint              = $CertificateThumbprint
            CertificatePath                    = $CertificatePath
            CertificatePassword                = $CertificatePassword
            ManagedIdentity                    = $ManagedIdentity.IsPresent
            AccessTokens                       = $AccessTokens
        }
        return $result
    }
    catch
    {
        New-M365DSCLogEntry -Message 'Error retrieving data:' `
            -Exception $_ `
            -Source $($MyInvocation.MyCommand.Source) `
            -TenantId $TenantId `
            -Credential $Credential

        throw
    }
}

function Set-TargetResource
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $AppId,

        [Parameter()]
        [Microsoft.Management.Infrastructure.CimInstance[]]
        $AppRoleAssignedTo,

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

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

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

        [Parameter()]
        [System.Boolean]
        $AccountEnabled,

        [Parameter()]
        [System.Boolean]
        $AppRoleAssignmentRequired,

        [Parameter()]
        [Microsoft.Management.Infrastructure.CimInstance]
        $ClaimsPolicy,

        [Parameter()]
        [Microsoft.Management.Infrastructure.CimInstance[]]
        $CustomSecurityAttributes,

        [Parameter()]
        [Microsoft.Management.Infrastructure.CimInstance[]]
        $DelegatedPermissionClassifications,

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

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

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

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

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

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

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

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

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

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

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

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

        [Parameter()]
        [Microsoft.Management.Infrastructure.CimInstance[]]
        $KeyCredentials,

        [Parameter()]
        [Microsoft.Management.Infrastructure.CimInstance[]]
        $PasswordCredentials,

        [Parameter()]
        [ValidateSet('Present', 'Absent')]
        [System.String]
        $Ensure = 'Present',

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

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

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

        [Parameter()]
        [System.Management.Automation.PSCredential]
        $ApplicationSecret,

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

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

        [Parameter()]
        [System.Management.Automation.PSCredential]
        $CertificatePassword,

        [Parameter()]
        [Switch]
        $ManagedIdentity,

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

    Write-Verbose -Message 'Setting configuration of Azure AD ServicePrincipal'
    #Ensure the proper dependencies are installed in the current environment.
    Confirm-M365DSCDependencies

    #region Telemetry
    $ResourceName = $MyInvocation.MyCommand.ModuleName -replace 'MSFT_', ''
    $CommandName = $MyInvocation.MyCommand
    $data = Format-M365DSCTelemetryParameters -ResourceName $ResourceName `
        -CommandName $CommandName `
        -Parameters $PSBoundParameters
    Add-M365DSCTelemetryEvent -Data $data
    #endregion

    $currentAADServicePrincipal = Get-TargetResource @PSBoundParameters
    $currentParameters = Remove-M365DSCAuthenticationParameter -BoundParameters $PSBoundParameters
    $currentParameters.Remove('ClaimsPolicy') | Out-Null
    $currentParameters.Remove('ObjectId') | Out-Null
    $currentParameters.Remove('Owners') | Out-Null
    $currentParameters.Remove('KeyCredentials') | Out-Null
    $currentParameters.Remove('PasswordCredentials') | Out-Null
    $currentParameters.Remove('DelegatedPermissionClassifications') | Out-Null
    $AppRoleAssignedToSpecified = $currentParameters.ContainsKey('AppRoleAssignedTo')
    $currentParameters.Remove('AppRoleAssignedTo') | Out-Null
    $currentParameters.Remove('LogoutUrl') | Out-Null

    # update the custom security attributes to be cmdlet comsumable
    if ($null -ne $currentParameters.CustomSecurityAttributes -and $currentParameters.CustomSecurityAttributes.Count -gt 0)
    {
        $currentSCAValue = Get-M365DSCAADServicePrincipalCustomSecurityAttributesAsCmdletHashtable -CustomSecurityAttributes $currentParameters.CustomSecurityAttributes
        $currentParameters.Remove('CustomSecurityAttributes') | Out-Null
        $currentParameters.Add('customSecurityAttributes', $currentSCAValue)
    }
    else
    {
        $currentParameters.Remove('CustomSecurityAttributes')
    }

    # If the AppId was passed as a display name (not in GUID format), translate it to an ID.
    if (-not [System.Guid]::TryParse($AppId, [ref][System.Guid]::Empty))
    {
        Write-Verbose -Message 'AppId was provided as a DisplayName. Translating it to an a GUID.'
        $appInstance = Get-MgApplication -Filter "DisplayName eq '$($AppId -replace "'", "''")'"
        $currentParameters.AppId = $appInstance.AppId
        $oldAppId = $AppId
        $AppId = $appInstance.AppId
        Write-Verbose -Message "Translated to AppId {$($currentParameters.AppId)}"
    }
    else
    {
        $appInstance = Get-MgApplication -Filter "AppId eq '$($AppId)'"
        if ($null -eq $appInstance)
        {
            throw "No application found with AppId or DisplayName matching '$AppId'."
        }
    }

    # ServicePrincipal should exist but it doesn't
    if ($Ensure -eq 'Present' -and $currentAADServicePrincipal.Ensure -eq 'Absent')
    {
        Write-Verbose -Message 'Creating new Service Principal'
        $newSP = New-MgServicePrincipal -BodyParameter $currentParameters
        Start-Sleep -Seconds 4

        # Assign Owners
        foreach ($owner in $Owners)
        {
            $userInfo = Get-MgUser -UserId $owner
            $body = @{
                '@odata.id' = (Get-MSCloudLoginConnectionProfile -Workload MicrosoftGraph).ResourceUrl + "v1.0/directoryObjects/$($userInfo.Id)"
            }
            Write-Verbose -Message "Adding new owner {$owner}"
            Invoke-M365DSCCommand -ScriptBlock { New-MgServicePrincipalOwnerByRef -ServicePrincipalId $newSP.Id -BodyParameter $body -ErrorAction Stop } -RetryOnNotFoundError -MaxRetries 4
        }

        # Adding delegated permissions classifications
        if ($null -ne $DelegatedPermissionClassifications)
        {
            foreach ($permissionClassification in $DelegatedPermissionClassifications)
            {
                $params = @{
                    classification = $permissionClassification.Classification
                    permissionName = $permissionClassification.permissionName
                }
                $Uri = (Get-MSCloudLoginConnectionProfile -Workload MicrosoftGraph).ResourceUrl + "v1.0/servicePrincipals(appId='$($currentParameters.AppId)')/delegatedPermissionClassifications"
                Invoke-M365DSCCommand -ScriptBlock { Invoke-MgGraphRequest -Uri $Uri -Method Post -Body $params -ErrorAction Stop } -RetryOnNotFoundError -MaxRetries 4
            }
        }

        # Update AppRoleAssignedTo
        if ($AppRoleAssignedToSpecified)
        {
            Write-Verbose -Message 'Updating AppRoleAssignedTo value'
            foreach ($assignment in $AppRoleAssignedTo)
            {
                if ($assignment.PrincipalType -eq 'User')
                {
                    Write-Verbose -Message "Retrieving user {$($assignment.Identity)}"
                    $user = Get-MgUser -Filter "startswith(UserPrincipalName, '$($assignment.Identity)')"
                    $PrincipalIdValue = $user.Id
                }
                else
                {
                    Write-Verbose -Message "Retrieving group {$($assignment.Identity)}"
                    $group = Get-MgGroup -Filter "DisplayName eq '$($assignment.Identity -replace "'", "''")'"
                    $PrincipalIdValue = $group.Id
                }

                $appRoleId = Get-M365DSCAADServicePrincipalAppRoleId -AppRoles $newSP.AppRoles -PrincipalType $assignment.PrincipalType
                $bodyParam = @{
                    principalId = $PrincipalIdValue
                    resourceId  = $newSP.Id
                    appRoleId   = $appRoleId
                }
                Write-Verbose -Message "Adding Service Principal AppRoleAssignedTo with values:`r`n$(ConvertTo-Json $bodyParam -Depth 3)"
                Invoke-M365DSCCommand -ScriptBlock { New-MgServicePrincipalAppRoleAssignedTo -ServicePrincipalId $newSP.Id -BodyParameter $bodyParam -ErrorAction Stop } -RetryOnNotFoundError -MaxRetries 4
            }
        }

        if ($PSBoundParameters.ContainsKey('ClaimsPolicy'))
        {
            Write-Verbose -Message 'Adding Claims Policy to the Service Principal'
            $claimsPolicyBody = Rename-M365DSCCimInstanceParameter -Properties $ClaimsPolicy
            Invoke-M365DSCCommand -ScriptBlock { Invoke-MgGraphRequest -Uri "/beta/servicePrincipals/$($newSP.Id)/claimsPolicy" -Method Put -Body $($claimsPolicyBody | ConvertTo-Json -Depth 20) -ErrorAction Stop } -RetryOnNotFoundError
        }
    }
    # ServicePrincipal should exist and will be configured to desired state
    elseif ($Ensure -eq 'Present' -and $currentAADServicePrincipal.Ensure -eq 'Present')
    {
        Write-Verbose -Message 'Updating existing Service Principal'
        $currentParameters.Remove("ReplyUrls") | Out-Null
        Write-Verbose -Message "CurrentParameters: $($currentParameters | Out-String)"
        Write-Verbose -Message "ServicePrincipalID: $($currentAADServicePrincipal.ObjectID)"

        if ($PreferredSingleSignOnMode -eq 'saml')
        {
            $IdentifierUris = $ServicePrincipalNames | Where-Object { $_ -notmatch $AppId -and $_ -notmatch $oldAppId }
            $currentParameters.Remove('ServicePrincipalNames')
        }

        #removing the current custom security attributes
        if ($currentAADServicePrincipal.CustomSecurityAttributes.Count -gt 0)
        {
            $currentAADServicePrincipal.CustomSecurityAttributes = Get-M365DSCAADServicePrincipalCustomSecurityAttributesAsCmdletHashtable -CustomSecurityAttributes $currentAADServicePrincipal.CustomSecurityAttributes -GetForDelete $true
            $CSAParams = @{
                customSecurityAttributes = $currentAADServicePrincipal.CustomSecurityAttributes
            }
            Invoke-MgGraphRequest -Uri ((Get-MSCloudLoginConnectionProfile -Workload MicrosoftGraph).ResourceUrl + "beta/servicePrincipals(appId='$($currentParameters.AppId)')") -Method Patch -Body $CSAParams
        }
        Update-MgServicePrincipal -ServicePrincipalId $currentAADServicePrincipal.ObjectID -BodyParameter $currentParameters

        if ($PSBoundParameters.ContainsKey('ClaimsPolicy'))
        {
            Write-Verbose -Message 'Updating Claims Policy on the Service Principal'
            $claimsPolicyBody = Rename-M365DSCCimInstanceParameter -Properties $ClaimsPolicy
            $null = Invoke-MgGraphRequest -Uri "/beta/servicePrincipals/$($currentAADServicePrincipal.ObjectID)/claimsPolicy" -Method Put -Body $($claimsPolicyBody | ConvertTo-Json -Depth 20)
        }

        if ($IdentifierUris)
        {
            Write-Verbose -Message 'Updating the Application ID Uri on the application instance.'
            $appInstance = Get-MgApplication -Filter "AppId eq '$AppId'"
            Update-MgApplication -ApplicationId $appInstance.Id -IdentifierUris $IdentifierUris
        }
        if ($AppRoleAssignedToSpecified)
        {
            Write-Verbose -Message 'Need to update AppRoleAssignedTo value'
            [Array]$currentPrincipals = $currentAADServicePrincipal.AppRoleAssignedTo.Identity
            [Array]$desiredPrincipals = $AppRoleAssignedTo.Identity

            if ($null -eq $currentPrincipals)
            {
                $currentPrincipals = @()
            }
            if ($null -eq $desiredPrincipals)
            {
                $desiredPrincipals = @()
            }

            [Array]$differences = Compare-Object -ReferenceObject $currentPrincipals -DifferenceObject $desiredPrincipals
            [Array]$membersToAdd = $differences | Where-Object -FilterScript { $_.SideIndicator -eq '=>' }
            [Array]$membersToRemove = $differences | Where-Object -FilterScript { $_.SideIndicator -eq '<=' }

            if ($differences.Count -gt 0)
            {
                if ($membersToAdd.Count -gt 0)
                {
                    $AppRoleAssignedToValues = @()
                    foreach ($assignment in $AppRoleAssignedTo)
                    {
                        $AppRoleAssignedToValues += @{
                            PrincipalType = $assignment.PrincipalType
                            Identity      = $assignment.Identity
                        }
                    }
                    foreach ($member in $membersToAdd)
                    {
                        $assignment = $AppRoleAssignedToValues | Where-Object -FilterScript { $_.Identity -eq $member.InputObject }
                        if ($assignment.PrincipalType -eq 'User')
                        {
                            Write-Verbose -Message "Retrieving user {$($assignment.Identity)}"
                            $user = Get-MgUser -Filter "startswith(UserPrincipalName, '$($assignment.Identity)')"
                            $PrincipalIdValue = $user.Id
                        }
                        else
                        {
                            Write-Verbose -Message "Retrieving group {$($assignment.Identity)}"
                            $group = Get-MgGroup -Filter "DisplayName eq '$($assignment.Identity -replace "'", "''")'"
                            $PrincipalIdValue = $group.Id
                        }

                        $appRoleId = Get-M365DSCAADServicePrincipalAppRoleId -AppRoles $appInstance.AppRoles -PrincipalType $assignment.PrincipalType
                        $bodyParam = @{
                            principalId = $PrincipalIdValue
                            resourceId  = $currentAADServicePrincipal.ObjectID
                            appRoleId   = $appRoleId
                        }
                        Write-Verbose -Message "Adding member {$($member.InputObject.ToString())}"
                        New-MgServicePrincipalAppRoleAssignedTo -ServicePrincipalId $currentAADServicePrincipal.ObjectID `
                            -BodyParameter $bodyParam | Out-Null
                    }
                }

                if ($membersToRemove.Count -gt 0)
                {
                    $AppRoleAssignedToValues = @()
                    foreach ($assignment in $currentAADServicePrincipal.AppRoleAssignedTo)
                    {
                        $AppRoleAssignedToValues += @{
                            PrincipalType = $assignment.PrincipalType
                            Identity      = $assignment.Identity
                        }
                    }
                    foreach ($member in $membersToRemove)
                    {
                        $assignment = $AppRoleAssignedToValues | Where-Object -FilterScript { $_.Identity -eq $member.InputObject }
                        if ($assignment.PrincipalType -eq 'User')
                        {
                            Write-Verbose -Message "Retrieving user {$($assignment.Identity)}"
                            $user = Get-MgUser -Filter "startswith(UserPrincipalName, '$($assignment.Identity)')"
                            $PrincipalIdValue = $user.Id
                        }
                        else
                        {
                            Write-Verbose -Message "Retrieving group {$($assignment.Identity)}"
                            $group = Get-MgGroup -Filter "DisplayName eq '$($assignment.Identity -replace "'", "''")'"
                            $PrincipalIdValue = $group.Id
                        }
                        Write-Verbose -Message "PrincipalID Value = '$PrincipalIdValue'"
                        Write-Verbose -Message "ServicePrincipalId = '$($currentAADServicePrincipal.ObjectID)'"
                        $allAssignments = Get-MgServicePrincipalAppRoleAssignedTo -ServicePrincipalId $currentAADServicePrincipal.ObjectID -All
                        $assignmentToRemove = $allAssignments | Where-Object -FilterScript { $_.PrincipalId -eq $PrincipalIdValue }
                        Write-Verbose -Message "Removing member {$($member.InputObject.ToString())}"
                        Remove-MgServicePrincipalAppRoleAssignedTo -ServicePrincipalId $currentAADServicePrincipal.ObjectID `
                            -AppRoleAssignmentId $assignmentToRemove.Id | Out-Null
                    }
                }
            }
        }

        Write-Verbose -Message 'Checking if owners need to be updated...'

        if ($null -ne $Owners)
        {
            $diffOwners = Compare-Object -ReferenceObject $currentAADServicePrincipal.Owners -DifferenceObject $Owners
        }
        foreach ($diff in $diffOwners)
        {
            $ownerInfo = Get-MgUser -UserId $diff.InputObject -ErrorAction SilentlyContinue
            if ($null -eq $ownerInfo)
            {
                $ownerInfo = Get-MgServicePrincipal -Filter "displayName eq '$($diff.InputObject -replace "'", "''")'" -ErrorAction SilentlyContinue
                if ($null -eq $ownerInfo)
                {
                    throw "Owner {$($diff.InputObject)} was not found as a user or service principal in the tenant."
                }
            }
            if ($diff.SideIndicator -eq '=>')
            {
                $body = @{
                    '@odata.id' = (Get-MSCloudLoginConnectionProfile -Workload MicrosoftGraph).ResourceUrl + "v1.0/directoryObjects/$($ownerInfo.Id)"
                }
                Write-Verbose -Message "Adding owner {$($ownerInfo.Id)}"
                New-MgServicePrincipalOwnerByRef -ServicePrincipalId $currentAADServicePrincipal.ObjectId `
                    -BodyParameter $body | Out-Null
            }
            else
            {
                Write-Verbose -Message "Removing owner {$($ownerInfo.Id)}"
                Remove-MgServicePrincipalOwnerDirectoryObjectByRef -ServicePrincipalId $currentAADServicePrincipal.ObjectId `
                    -DirectoryObjectId $ownerInfo.Id | Out-Null
            }
        }

        Write-Verbose -Message 'Checking if DelegatedPermissionClassifications need to be updated...'

        if ($null -ne $DelegatedPermissionClassifications)
        {
            # removing old perm classifications
            $Uri = (Get-MSCloudLoginConnectionProfile -Workload MicrosoftGraph).ResourceUrl + "v1.0/servicePrincipals(appId='$($currentParameters.AppId)')/delegatedPermissionClassifications"
            $permissionClassificationList = Invoke-MgGraphRequest -Uri $Uri -Method Get
            foreach ($permissionClassification in $permissionClassificationList.Value)
            {
                Invoke-MgGraphRequest -Uri "$($Uri)/$($permissionClassification.Id)" -Method Delete
            }

            # adding new perm classifications
            foreach ($permissionClassification in $DelegatedPermissionClassifications)
            {
                $params = @{
                    classification = $permissionClassification.Classification
                    permissionName = $permissionClassification.permissionName
                }
                Invoke-MgGraphRequest -Uri $Uri -Method Post -Body $params
            }
        }
    }
    # ServicePrincipal exists but should not
    elseif ($Ensure -eq 'Absent' -and $currentAADServicePrincipal.Ensure -eq 'Present')
    {
        Write-Verbose -Message 'Removing Service Principal'
        Remove-MgServicePrincipal -ServicePrincipalId $currentAADServicePrincipal.ObjectID
    }
}

function Test-TargetResource
{
    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $AppId,

        [Parameter()]
        [Microsoft.Management.Infrastructure.CimInstance[]]
        $AppRoleAssignedTo,

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

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

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

        [Parameter()]
        [System.Boolean]
        $AccountEnabled,

        [Parameter()]
        [System.Boolean]
        $AppRoleAssignmentRequired,

        [Parameter()]
        [Microsoft.Management.Infrastructure.CimInstance]
        $ClaimsPolicy,

        [Parameter()]
        [Microsoft.Management.Infrastructure.CimInstance[]]
        $CustomSecurityAttributes,

        [Parameter()]
        [Microsoft.Management.Infrastructure.CimInstance[]]
        $DelegatedPermissionClassifications,

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

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

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

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

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

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

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

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

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

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

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

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

        [Parameter()]
        [Microsoft.Management.Infrastructure.CimInstance[]]
        $KeyCredentials,

        [Parameter()]
        [Microsoft.Management.Infrastructure.CimInstance[]]
        $PasswordCredentials,

        [Parameter()]
        [ValidateSet('Present', 'Absent')]
        [System.String]
        $Ensure = 'Present',

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

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

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

        [Parameter()]
        [System.Management.Automation.PSCredential]
        $ApplicationSecret,

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

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

        [Parameter()]
        [System.Management.Automation.PSCredential]
        $CertificatePassword,

        [Parameter()]
        [Switch]
        $ManagedIdentity,

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

    $null = New-M365DSCConnection -Workload 'MicrosoftGraph' `
        -InboundParameters $PSBoundParameters

    #Ensure the proper dependencies are installed in the current environment.
    Confirm-M365DSCDependencies

    #region Telemetry
    $ResourceName = $MyInvocation.MyCommand.ModuleName.Replace('MSFT_', '')
    $CommandName = $MyInvocation.MyCommand
    $data = Format-M365DSCTelemetryParameters -ResourceName $ResourceName `
        -CommandName $CommandName `
        -Parameters $PSBoundParameters
    Add-M365DSCTelemetryEvent -Data $data
    #endregion

    $compareParameters = Get-CompareParameters
    $result = Test-M365DSCTargetResource -DesiredValues $PSBoundParameters `
        -ResourceName $($MyInvocation.MyCommand.Source).Replace('MSFT_', '') `
        @compareParameters
    return $result
}

function Export-TargetResource
{
    [CmdletBinding()]
    [OutputType([System.String])]
    param
    (
        [Parameter()]
        [System.String]
        $Filter,

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

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

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

        [Parameter()]
        [System.Management.Automation.PSCredential]
        $ApplicationSecret,

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

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

        [Parameter()]
        [System.Management.Automation.PSCredential]
        $CertificatePassword,

        [Parameter()]
        [Switch]
        $ManagedIdentity,

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

    $ConnectionMode = New-M365DSCConnection -Workload 'MicrosoftGraph' `
        -InboundParameters $PSBoundParameters

    #Ensure the proper dependencies are installed in the current environment.
    Confirm-M365DSCDependencies

    #region Telemetry
    $ResourceName = $MyInvocation.MyCommand.ModuleName -replace 'MSFT_', ''
    $CommandName = $MyInvocation.MyCommand
    $data = Format-M365DSCTelemetryParameters -ResourceName $ResourceName `
        -CommandName $CommandName `
        -Parameters $PSBoundParameters
    Add-M365DSCTelemetryEvent -Data $data
    #endregion

    $dscContent = [System.Text.StringBuilder]::new()
    try
    {
        $i = 1
        Write-M365DSCHost -Message "`r`n" -DeferWrite
        [array] $exportedInstances = Get-MgServicePrincipal -All `
            -Filter $Filter `
            -Expand 'AppRoleAssignedTo' `
            -Property $Script:PropertiesToExport `
            -ErrorAction Stop
        foreach ($AADServicePrincipal in $exportedInstances)
        {
            if ($null -ne $Global:M365DSCExportResourceInstancesCount)
            {
                $Global:M365DSCExportResourceInstancesCount++
            }

            Write-M365DSCHost -Message " |---[$i/$($exportedInstances.Count)] $($AADServicePrincipal.DisplayName)" -DeferWrite
            $Params = @{
                Credential            = $Credential
                ApplicationId         = $ApplicationId
                ApplicationSecret     = $ApplicationSecret
                TenantId              = $TenantId
                CertificateThumbprint = $CertificateThumbprint
                CertificatePath       = $CertificatePath
                CertificatePassword   = $CertificatePassword
                ManagedIdentity       = $ManagedIdentity.IsPresent
                AppID                 = $AADServicePrincipal.DisplayName
                AccessTokens          = $AccessTokens
            }
            $Script:exportedInstance = $AADServicePrincipal
            $Results = Get-TargetResource @Params
            if ($Results.Ensure -eq 'Present')
            {
                if ($Results.AppRoleAssignedTo.Count -gt 0)
                {
                    $complexTypeStringResult = Get-M365DSCDRGComplexTypeToString `
                        -ComplexObject $Results.AppRoleAssignedTo `
                        -CIMInstanceName 'AADServicePrincipalRoleAssignment'
                    if (-not [String]::IsNullOrWhiteSpace($complexTypeStringResult))
                    {
                        $Results.AppRoleAssignedTo = $complexTypeStringResult
                    }
                    else
                    {
                        $Results.Remove('AppRoleAssignedTo') | Out-Null
                    }
                }
                if ($null -ne $Results.ClaimsPolicy)
                {
                    $complexMapping = @(
                        @{
                            Name            = 'ClaimsMappingPolicy'
                            CimInstanceName = 'AADServicePrincipalClaimsMappingPolicy'
                            IsRequired      = $False
                        },
                        @{
                            Name            = 'claims'
                            CimInstanceName = 'AADServicePrincipalCustomClaim'
                            IsRequired      = $False
                        },
                        @{
                            Name            = 'groupFilter'
                            CimInstanceName = 'AADServicePrincipalClaimsPolicyGroupFilter'
                            IsRequired      = $False
                        },
                        @{
                            Name            = 'input'
                            CimInstanceName = 'MSFT_AADServicePrincipalTransformationAttribute'
                            IsRequired      = $False
                        },
                        @{
                            Name            = 'configurations'
                            CimInstanceName = 'AADServicePrincipalCustomClaimConfiguration'
                            IsRequired      = $False
                        },
                        @{
                            Name            = 'attribute'
                            CimInstanceName = 'AADServicePrincipalCustomClaimAttribute'
                            IsRequired      = $False
                        },
                        @{
                            Name            = 'condition'
                            CimInstanceName = 'AADServicePrincipalCustomClaimCondition'
                            IsRequired      = $False
                        },
                        @{
                            Name            = 'transformations'
                            CimInstanceName = 'AADServicePrincipalCustomClaimTransformation'
                            IsRequired      = $False
                        }
                    )
                    $complexTypeStringResult = Get-M365DSCDRGComplexTypeToString `
                        -ComplexObject $Results.ClaimsPolicy `
                        -CIMInstanceName 'AADServicePrincipalClaimsPolicy' `
                        -ComplexTypeMapping $complexMapping
                    if (-not [String]::IsNullOrWhiteSpace($complexTypeStringResult))
                    {
                        $Results.ClaimsPolicy = $complexTypeStringResult
                    }
                    else
                    {
                        $Results.Remove('ClaimsPolicy') | Out-Null
                    }
                }
                if ($Results.DelegatedPermissionClassifications.Count -gt 0)
                {
                    $complexTypeStringResult = Get-M365DSCDRGComplexTypeToString `
                        -ComplexObject $Results.DelegatedPermissionClassifications `
                        -CIMInstanceName 'AADServicePrincipalDelegatedPermissionClassification' -IsArray:$true
                    if (-not [String]::IsNullOrWhiteSpace($complexTypeStringResult))
                    {
                        $Results.DelegatedPermissionClassifications = $complexTypeStringResult
                    }
                    else
                    {
                        $Results.Remove('DelegatedPermissionClassifications') | Out-Null
                    }
                }
                if ($null -ne $Results.KeyCredentials)
                {
                    $complexTypeStringResult = Get-M365DSCDRGComplexTypeToString `
                        -ComplexObject $Results.KeyCredentials `
                        -CIMInstanceName 'MicrosoftGraphkeyCredential' -IsArray:$true
                    if (-not [String]::IsNullOrWhiteSpace($complexTypeStringResult))
                    {
                        $Results.KeyCredentials = $complexTypeStringResult
                    }
                    else
                    {
                        $Results.Remove('KeyCredentials') | Out-Null
                    }
                }
                if ($null -ne $Results.PasswordCredentials)
                {
                    $complexTypeStringResult = Get-M365DSCDRGComplexTypeToString `
                        -ComplexObject $Results.PasswordCredentials `
                        -CIMInstanceName 'MicrosoftGraphpasswordCredential' -IsArray:$true
                    if (-not [String]::IsNullOrWhiteSpace($complexTypeStringResult))
                    {
                        $Results.PasswordCredentials = $complexTypeStringResult
                    }
                    else
                    {
                        $Results.Remove('PasswordCredentials') | Out-Null
                    }
                }
                if ($Results.CustomSecurityAttributes.Count -gt 0)
                {
                    $complexMapping = @(
                        @{
                            Name            = 'CustomSecurityAttributes'
                            CimInstanceName = 'AADServicePrincipalAttributeSet'
                            IsRequired      = $False
                        },
                        @{
                            Name            = 'AttributeValues'
                            CimInstanceName = 'AADServicePrincipalAttributeValue'
                            IsRequired      = $False
                        }
                    )
                    $complexTypeStringResult = Get-M365DSCDRGComplexTypeToString `
                        -ComplexObject $Results.CustomSecurityAttributes `
                        -CIMInstanceName 'AADServicePrincipalAttributeSet' `
                        -ComplexTypeMapping $complexMapping `
                        -IsArray:$true
                    if (-not [String]::IsNullOrWhiteSpace($complexTypeStringResult))
                    {
                        $Results.CustomSecurityAttributes = $complexTypeStringResult
                    }
                    else
                    {
                        $Results.Remove('CustomSecurityAttributes') | Out-Null
                    }
                }
                $currentDSCBlock = Get-M365DSCExportContentForResource -ResourceName $ResourceName `
                    -ConnectionMode $ConnectionMode `
                    -ModulePath $PSScriptRoot `
                    -Results $Results `
                    -Credential $Credential `
                    -NoEscape @('AppRoleAssignedTo', 'ClaimsPolicy', 'DelegatedPermissionClassifications', 'KeyCredentials', 'PasswordCredentials', 'CustomSecurityAttributes')

                [void]$dscContent.Append($currentDSCBlock)
                Save-M365DSCPartialExport -Content $currentDSCBlock `
                    -FileName $Global:PartialExportFileName

                Write-M365DSCHost -Message $Global:M365DSCEmojiGreenCheckMark -CommitWrite
                $i++
            }
        }
        return $dscContent.ToString()
    }
    catch
    {
        New-M365DSCLogEntry -Message 'Error during Export:' `
            -Exception $_ `
            -Source $($MyInvocation.MyCommand.Source) `
            -TenantId $TenantId `
            -Credential $Credential

        throw
    }
}

function Get-M365DSCAADServicePrincipalCustomSecurityAttributesAsCmdletHashtable
{
    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable])]
    param(
        [Parameter(Mandatory = $true)]
        [System.Collections.ArrayList]
        $CustomSecurityAttributes,

        [Parameter()]
        [System.Boolean]
        $GetForDelete = $false
    )

    # logic to update the custom security attributes to be cmdlet comsumable
    $updatedCustomSecurityAttributes = @{}
    foreach ($attributeSet in $CustomSecurityAttributes)
    {
        $attributeSetKey = $attributeSet.AttributeSetName

        $valuesHashtable = @{}
        $valuesHashtable.Add('@odata.type', '#Microsoft.DirectoryServices.CustomSecurityAttributeValue')
        foreach ($attribute in $attributeSet.AttributeValues)
        {
            $attributeKey = $attribute.AttributeName
            # supply attributeName = $null in the body, if you want to delete this attribute
            if ($GetForDelete -eq $true)
            {
                $valuesHashtable.Add($attributeKey, $null)
                continue
            }

            $odataKey = $attributeKey + '@odata.type'

            if ($null -ne $attribute.StringArrayValue)
            {
                $valuesHashtable.Add($odataKey, '#Collection(String)')
                $attributeValue = $attribute.StringArrayValue
            }
            elseif ($null -ne $attribute.IntArrayValue)
            {
                $valuesHashtable.Add($odataKey, '#Collection(Int32)')
                $attributeValue = $attribute.IntArrayValue
            }
            elseif ($null -ne $attribute.StringValue)
            {
                $valuesHashtable.Add($odataKey, '#String')
                $attributeValue = $attribute.StringValue
            }
            elseif ($null -ne $attribute.IntValue)
            {
                $valuesHashtable.Add($odataKey, '#Int32')
                $attributeValue = $attribute.IntValue
            }
            elseif ($null -ne $attribute.BoolValue)
            {
                $attributeValue = $attribute.BoolValue
            }

            $valuesHashtable.Add($attributeKey, $attributeValue)
        }
        $updatedCustomSecurityAttributes.Add($attributeSetKey, $valuesHashtable)
    }
    return $updatedCustomSecurityAttributes
}

# Function to create MSFT_AttributeValue
function New-AttributeValue
{
    param
    (
        [string]$AttributeName,
        [object]$Value
    )

    $attributeValue = @{
        AttributeName    = $AttributeName
        StringArrayValue = $null
        IntArrayValue    = $null
        StringValue      = $null
        IntValue         = $null
        BoolValue        = $null
    }

    # Handle different types of values
    if ($Value -is [string])
    {
        $attributeValue.StringValue = $Value
    }
    elseif ($Value -is [System.Int32] -or $Value -is [System.Int64])
    {
        $attributeValue.IntValue = $Value
    }
    elseif ($Value -is [bool])
    {
        $attributeValue.BoolValue = $Value
    }
    elseif ($Value -is [array])
    {
        if ($Value[0] -is [string])
        {
            $attributeValue.StringArrayValue = $Value
        }
        elseif ($Value[0] -is [System.Int32] -or $Value[0] -is [System.Int64])
        {
            $attributeValue.IntArrayValue = $Value
        }
    }

    return $attributeValue
}

function Get-CustomSecurityAttributes
{
    [OutputType([System.Array])]
    param
    (
        $ServicePrincipal
    )

    $customSecurityAttributes = $ServicePrincipal.customSecurityAttributes
    $newCustomSecurityAttributes = @()

    foreach ($key in $customSecurityAttributes.Keys)
    {
        $attributeSet = @{
            AttributeSetName = $key
            AttributeValues  = @()
        }

        foreach ($attribute in $customSecurityAttributes[$key].Keys)
        {
            # Skip properties that end with '@odata.type'
            if ($attribute -like '*@odata.type')
            {
                continue
            }

            $value = $customSecurityAttributes[$key][$attribute]
            $attributeName = $attribute # Keep the attribute name as it is

            # Create the attribute value and add it to the set
            $attributeSet.AttributeValues += New-AttributeValue -AttributeName $attributeName -Value $value
        }

        #Add the attribute set to the final structure
        $newCustomSecurityAttributes += $attributeSet
    }

    # Display the new structure
    return [Array]$newCustomSecurityAttributes
}

function Get-CompareParameters
{
    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable])]
    param()

    return @{
        ExcludedProperties = @('ObjectId', 'KeyCredentials', 'PasswordCredentials', 'ReplyUrls', 'LogoutUrl')
    }
}

function Get-M365DSCAADServicePrincipalAppRoleId
{
    [CmdletBinding()]
    [OutputType([System.String])]
    param(
        [Parameter()]
        [AllowNull()]
        [System.Object[]]
        $AppRoles,

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

    $appRoleId = ($AppRoles | Where-Object -FilterScript { $_.DisplayName -eq $PrincipalType } | Select-Object -First 1).Id
    if ([System.String]::IsNullOrEmpty($appRoleId))
    {
        $appRoleId = '00000000-0000-0000-0000-000000000000'
    }

    return $appRoleId
}

Export-ModuleMember -Function @('*-TargetResource', 'Get-CompareParameters')