EntraPolicySuite.psm1

Function AddGroupMemberOf_GMSA_Group_AD



Function AddMembers_GMSA_Group_AD



Function BreakGlassValidation {
    [CmdletBinding()]
    param(
        [Parameter()]
        [object]$BreakGlassAccountsGroup,
        [Parameter()]
        [object]$BreakGlassAccounts

    )

    If (!($BreakGlassAccountsGroup))
        {
            Write-host ""
            Write-host "Break Glass Accounts Group variable is empty ..... terminating !!"
            Write-host ""
            Break
        }
    Else
        {
            Write-host ""
            Write-host "Break Glass Accounts Group variable is OK !"
            Write-host ""
        }


    If (!($BreakGlassAccounts))
        {
            Write-host ""
            Write-host "Break Glass Accounts variable is empty ..... terminating !!"
            Write-host ""
            Break
        }
    Else
        {
            Write-host ""
            Write-host "Break Glass Accounts variable is OK !"
            Write-host ""
        }
}



Function Check-GroupMembers {
    param(
        [Parameter(Mandatory)]
        [string]$GroupId
    )

$MembersCount = 0
    try {
        # Attempt to retrieve the first member of the group
        $members = Get-MgGroupMember -GroupId $GroupId

        if ($members) {
            $MembersCount = $Members.count
            Write-verbose "Group with ID $GroupId has members."
        } else {
            $MembersCount = 0
            Write-verbose "Group with ID $GroupId has no members."
        }
    } catch {
        Write-Error "Error retrieving members for group with ID GroupId: $_"
    }

    Return $MembersCount
}


Function Check-GroupMembers {
    param(
        [Parameter(Mandatory)]
        [string]$GroupId
    )

$MembersCount = 0
    try {
        # Attempt to retrieve the first member of the group
        $members = Get-MgGroupMember -GroupId $GroupId

        if ($members) {
            $MembersCount = $Members.count
            Write-verbose "Group with ID $GroupId has members."
        } else {
            $MembersCount = 0
            Write-verbose "Group with ID $GroupId has no members."
        }
    } catch {
        Write-Error "Error retrieving members for group with ID GroupId: $_"
    }

    Return $MembersCount
}



Function CheckAccountConditions {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [object]$User,
        [Parameter(Mandatory)] [string]$Persona,
        [Parameter(Mandatory)] [string]$TagType,
        [Parameter(Mandatory)] [string]$TagValueAD,
        [Parameter(Mandatory)] [string]$TagValueCloud,
        [Parameter(Mandatory)] [string]$ConditionsType,
        [Parameter()] [AllowNull()] [string]$ConditionGroup,
        [Parameter(Mandatory)] [array]$Target,
        [Parameter()] [AllowNull()] [string]$OnPremisesSyncEnabled,
        [Parameter()] [object]$MailboxInfo,
        [Parameter()] [object]$TeamsRoom
    )

    [boolean]$ConditionMet = $false
    $ModifiedTagValue = $null

    Write-Verbose ""
    Write-Verbose "Checking ..."
    Write-Verbose "ConditionsType : $($ConditionsType)"
    Write-Verbose "Target : $($Target)"
    Write-Verbose "ConditionGroup : $($ConditionGroup)"

    switch ($ConditionsType) {
        "UPN_Like" {
            if ($User.UserPrincipalName -Like "$($Target)") {
                write-verbose $User.UserPrincipalName
                Write-Verbose "UPN_Like $($Target) = true"
                $ConditionMet = $true
            }
        }
        "UPN_NotLike" {
            if ($User.UserPrincipalName -Notlike "$($Target)") {
                write-verbose $User.UserPrincipalName
                Write-Verbose "UPN_NotLike $($Target) = true"
                $ConditionMet = $true
            }
        }
        "MemberOfGroup" {
            If ($User.OnPremisesSyncEnabled) {
                $GroupMembers = $global:AD_Group_Members_HashTable[$Target]
                if ($GroupMembers) {
                    if ($User.UserPrincipalName -in $GroupMembers.Members.UserPrincipalName) {
                        Write-Verbose "MemberOfGroup $($Filter) = true"
                        $ConditionMet = $true
                    }
                }
            } Else {
                $GroupMembers = $global:Entra_Group_Members_HashTable[$Target]
                if ($GroupMembers) {
                    if ($User.Id -in $GroupMembers.members.id) {
                        Write-Verbose "MemberOfGroup $($Filter) = true"
                        $ConditionMet = $true
                    }
                }
            }
            
        }
        "AD_OU_DN_Like" {
            if ($User.OnPremisesDistinguishedName -Like "$($Target)") {
                write-verbose $User.OnPremisesDistinguishedName
                Write-Verbose "AD_OU_DN_Like $($Filter) = true"
                $ConditionMet = $true
            }
        }
        "AD_OU_DN_NotLike" {
            if ( ($User.OnPremisesDistinguishedName -NotLike "$($Target)") -and ($User.OnPremisesDistinguishedName) ) {
                write-verbose $User.OnPremisesDistinguishedName
                Write-Verbose "AD_OU_DN_NotLike $($Filter) = true"
                $ConditionMet = $true
            }
        }
        "OnPremisesSyncEnabled" {
                if ( ($Target -match "TRUE") -and ($User.OnPremisesSyncEnabled) ) {
                    Write-Verbose "OnPremisesSyncEnabled $($Filter) = true"
                    $ConditionMet = $true
                } elseif ( ($Target -match "FALSE") -and (-not $User.OnPremisesSyncEnabled) )  {
                    # Write-Host "NOT OnPremisesSyncEnabled $($Filter) = true"
                    $ConditionMet = $true
                }
        }
        "MobilePhone_Like" {
                if ( ($User.MobilePhone -Like "$($Target)") -and ($User.MobilePhone -ne $null) ) {
                    write-verbose $User.MobilePhone
                    Write-Verbose "MobilePhone_Like $($Filter) = true"
                    $ConditionMet = $true
                }
        }
        "GivenName_Like" {
                if ( ($User.GivenName -Like "$($Target)") -and ($User.GivenName -ne $null) ) {
                    write-verbose $User.GivenName
                    Write-Verbose "GivenName $($Filter) = true"
                    $ConditionMet = $true
                }
        }
        "SurName_Like" {
                if ( ($User.SurName -Like "$($Target)") -and ($User.SurName -ne $null) ) {
                    write-verbose $User.SurName
                    Write-Verbose "SurName_Like $($Filter) = true"
                    $ConditionMet = $true
                }
        }
        "UserType_Like" {
                if ( ($User.UserType -Like "$($Target)") -and ($User.UserType -ne $null) ) {
                    write-verbose $User.UserType
                    Write-Verbose "UserType_Like $($Filter) = true"
                    $ConditionMet = $true
                }
        }
        "UserType_NotLike" {
                if ( ($User.UserType -NotLike "$($Target)") -and ($User.UserType -ne $null) ) {
                    write-verbose $User.UserType
                    Write-Verbose "UserType_NotLike $($Filter) = true"
                    $ConditionMet = $true
                }
        }
        "EmployeeType_Like" {
                if ( ($User.EmployeeType -Like "$($Target)") -and ($User.EmployeeType -ne $null) ) {
                    write-verbose $User.EmployeeType
                    Write-Verbose "EmployeeType_Like $($Filter) = true"
                    $ConditionMet = $true
                }
        }
        "Teams_Room_Like" {
                if ( ($Target -match "TRUE") -and ($TeamsRoom) ) {
                    Write-Verbose "Teams_Room_Like $($Filter) = true"
                    $ConditionMet = $true
                } elseif ( ($Target -match "FALSE") -and (-not $TeamsRoom) )  {
                    Write-Verbose "Teams_Room_Like $($Filter) = true"
                    $ConditionMet = $true
                }
        }
        "Mailbox_RecipientTypeDetails_Like" {
                if ( ($MailboxInfo.RecipientTypeDetails -Like "$($Target)") -and ($MailBOxInfo.RecipientTypeDetails -ne $null) ) {
                    write-verbose $MailboxInfo.RecipientTypeDetails
                    Write-Verbose "Mailbox_RecipientTypeDetails_Like $($Filter) = true"
                    $ConditionMet = $true
                }
        }
        "Mailbox_RecipientTypeDetails_NotLike" {
                if ( ($MailboxInfo.RecipientTypeDetails -NotLike "$($Target)") -and ($MailBOxInfo.RecipientTypeDetails -ne $null) ) {
                    write-verbose $MailboxInfo.RecipientTypeDetails
                    Write-Verbose "Mailbox_RecipientTypeDetails_NotLike $($Filter) = true"
                    $ConditionMet = $true
                }
        }
        "Mailbox_RecipientTypeDetails_ModifiedTagValue_Classification" {
                If ($MailboxInfo.RecipientTypeDetails) {
                    write-verbose $MailboxInfo.RecipientTypeDetails
                    Write-Verbose "Mailbox_RecipientTypeDetails_ModifiedTagValue $($Filter) = true"
                    $ConditionMet = $true
                    $ModifiedTagValue = "Exchange_" + $MailboxInfo.RecipientTypeDetails
                }
        }
        "Mailbox_RecipientTypeDetails_ModifiedTagValue_Authentication" {
                If ($MailboxInfo.RecipientTypeDetails) {
                    write-verbose $MailboxInfo.RecipientTypeDetails
                    Write-Verbose "Mailbox_RecipientTypeDetails_ModifiedTagValue $($Filter) = true"
                    $ConditionMet = $true
                    $ModifiedTagValue = "Exchange_" + $MailboxInfo.RecipientTypeDetails + "_NoSignin"
                }
        }
        default {
            Write-Host "Unknown condition type: $ConditionsType"
        }
    }

    # write-host $ConditionMet
    Return $ConditionMet,$ModifiedTagValue
}



Function CheckAccountTagUserAuthentication



Function CheckAccountTagUserCAPilot



Function CheckAccountTagUserClassification



Function CheckDeviceConditions {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [object]$Device,
        [Parameter(Mandatory)] [string]$Persona,
        [Parameter(Mandatory)] [string]$TagType,
        [Parameter(Mandatory)] [AllowEmptyString()] [AllowNull()] [string]$TagValueAD,
        [Parameter(Mandatory)] [AllowEmptyString()] [AllowNull()] [string]$TagValueCloud,
        [Parameter(Mandatory)] [string]$ConditionsType,
        [Parameter()] [AllowNull()] [string]$ConditionGroup,
        [Parameter(Mandatory)] [array]$Target,
        [Parameter()] [AllowNull()] [string]$OnPremisesSyncEnabled
    )

    [boolean]$ConditionMet = $false
    $ModifiedTagValue = $null

    Write-Verbose "Checking condition $ConditionsType against device $($Device.DisplayName)..."

    # https://learn.microsoft.com/en-us/graph/api/resources/device?view=graph-rest-1.0
    switch ($ConditionsType) {
        "AccountEnabled" {
            if (($Target -eq "TRUE" -and $Device.AccountEnabled) -or ($Target -eq "FALSE" -and -not $Device.AccountEnabled)) {
                Write-Verbose "AccountEnabled matches $Target"
                $ConditionMet = $true
            }
        }
        "DeviceCategory_Like" {
            if ($Device.DeviceCategory -Like $Target) {
                Write-Verbose "DeviceCategory matches $Target"
                $ConditionMet = $true
            }
        }
        "DeviceCategory_NotLike" {
            if ($Device.DeviceCategory -NotLike $Target) {
                Write-Verbose "DeviceCategory matches $Target"
                $ConditionMet = $true
            }
        }
        "DeviceId" {
            if ($Device.DeviceId -eq $Target) {
                Write-Verbose "DeviceId matches $Target"
                $ConditionMet = $true
            }
        }

        # unknown, company, personal
        "DeviceOwnership_Like" {
            if ($Device.DeviceOwnership -like $Target) {
                Write-Verbose "DeviceOwnership matches $Target"
                $ConditionMet = $true
            }
        }
        "DeviceOwnership_NotLike" {
            if ($Device.DeviceOwnership -NotLike $Target) {
                Write-Verbose "DeviceOwnership doesn't match $Target"
                $ConditionMet = $true
            }
        }
        "DisplayName_Like" {
            if ($Device.DisplayName -like "$($Target)") {
                Write-Verbose "DisplayName matches $Target"
                $ConditionMet = $true
            }
        }
        "DisplayName_NotLike" {
            if ($Device.DisplayName -Notlike "$($Target)") {
                Write-Verbose "DisplayName doesn't match $Target"
                $ConditionMet = $true
            }
        }

        #Apple Device Enrollment Profile, Device enrollment - Corporate device identifiers, or Windows Autopilot profile name.
        "EnrollmentProfileName_Like" {
            if ($Device.EnrollmentProfileName -Like $Target) {
                Write-Verbose "EnrollmentProfileName matches $Target"
                $ConditionMet = $true
            }
        }
        "EnrollmentProfileName_NotLike" {
            if ($Device.EnrollmentProfileName -NotLike $Target) {
                Write-Verbose "EnrollmentProfileName doesn't match $Target"
                $ConditionMet = $true
            }
        }

        # unknown, userEnrollment, deviceEnrollmentManager, appleBulkWithUser, appleBulkWithoutUser, windowsAzureADJoin, windowsBulkUserless, windowsAutoEnrollment,
        # windowsBulkAzureDomainJoin, windowsCoManagement, windowsAzureADJoinUsingDeviceAuth,appleUserEnrollment, appleUserEnrollmentWithServiceAccount.
        "EnrollmentType_Like" {
            if ($Device.EnrollmentType -Like $Target) {
                Write-Verbose "EnrollmentType matches $Target"
                $ConditionMet = $true
            }
        }
        "EnrollmentType_NotLike" {
            if ($Device.EnrollmentType -NotLike $Target) {
                Write-Verbose "EnrollmentType doesn't match $Target"
                $ConditionMet = $true
            }
        }
        "Id" {
            if ($Device.Id -eq $Target) {
                Write-Verbose "Id matches $Target"
                $ConditionMet = $true
            }
        }
        "IsCompliant" {
            if (($Target -eq "TRUE" -and $Device.IsCompliant) -or ($Target -eq "FALSE" -and -not $Device.IsCompliant)) {
                Write-Verbose "Device compliance matches $Target"
                $ConditionMet = $true
            }
        }
        "IsManaged" {
            if (($Target -eq "TRUE" -and $Device.IsManaged) -or ($Target -eq "FALSE" -and -not $Device.IsManaged)) {
                Write-Verbose "Device management status matches $Target"
                $ConditionMet = $true
            }
        }
        "Manufacturer_Like" {
            if ($Device.Manufacturer -Like $Target) {
                Write-Verbose "Manufacturer matches $Target"
                $ConditionMet = $true
            }
        }
        "Manufacturer_NotLike" {
            if ($Device.Manufacturer -NotLike $Target) {
                Write-Verbose "Manufacturer doesn't match $Target"
                $ConditionMet = $true
            }
        }
        
        # eas, mdm, easMdm, intuneClient, easIntuneClient, configurationManagerClient,
        # configurationManagerClientMdm, configurationManagerClientMdmEas, unknown, jamf, googleCloudDevicePolicyController
        "ManagementType_Like" {
            if ($Device.ManagementType -Like $Target) {
                Write-Verbose "ManagementType matches $Target"
                $ConditionMet = $true
            }
        }
        "ManagementType_NotLike" {
            if ($Device.ManagementType -NotLike $Target) {
                Write-Verbose "ManagementType doesn't match $Target"
                $ConditionMet = $true
            }
        }

        "mdmAppId_Like" {
            if ($Device.mdmAppId -Like $Target) {
                Write-Verbose "mdmAppId matches $Target"
                $ConditionMet = $true
            }
        }
        "mdmAppId_NotLike" {
            if ($Device.mdmAppId -NotLike $Target) {
                Write-Verbose "mdmAppId doesn't match $Target"
                $ConditionMet = $true
            }
        }
        "Model_Like" {
            if ($Device.Model -Like $Target) {
                Write-Verbose "Model matches $Target"
                $ConditionMet = $true
            }
        }
        "Model_NotLike" {
            if ($Device.Model -NotLike $Target) {
                Write-Verbose "Model doesn't match $Target"
                $ConditionMet = $true
            }
        }
        "OnPremisesSyncEnabled" {
            if (($Target -eq "TRUE" -and $Device.OnPremisesSyncEnabled) -or ($Target -eq "FALSE" -and -not $Device.OnPremisesSyncEnabled)) {
                Write-Verbose "OnPremisesSyncEnabled matches $Target"
                $ConditionMet = $true
            }
        }
        "OperatingSystem_Like" {
            if ($Device.OperatingSystem -like "$($Target)") {
                Write-Verbose "OperatingSystem matches $Target"
                $ConditionMet = $true
            }
        }
        "OperatingSystem_NotLike" {
            if ($Device.OperatingSystem -Notlike "$($Target)") {
                Write-Verbose "OperatingSystem doesn't match $Target"
                $ConditionMet = $true
            }
        }
        "OperatingSystemVersion_Like" {
            if ($Device.OperatingSystemVersion -like "$($Target)") {
                Write-Verbose "OperatingSystemVersion matches $Target"
                $ConditionMet = $true
            }
        }
        "OperatingSystemVersion_NotLike" {
            if ($Device.OperatingSystemVersion -Notlike "$($Target)") {
                Write-Verbose "OperatingSystemVersion doesn't match $Target"
                $ConditionMet = $true
            }
        }
        "PhysicalIds_Contains" {
            if ($Device.PhysicalIds -contains $Target) {
                Write-Verbose "PhysicalIDs contains $Target"
                $ConditionMet = $true
            }
        }
        "PhysicalIds_NotContains" {
            if (-not ($Device.PhysicalIds -contains $Target)) {
                Write-Verbose "PhysicalIDs doesn't contain $Target"
                $ConditionMet = $true
            }
        }
        
        # RegisteredDevice (default), SecureVM, Printer, Shared, IoT.
        "ProfileType_Like" {
            if ($Device.ProfileType -Like $Target) {
                Write-Verbose "ProfileType matches $Target"
                $ConditionMet = $true
            }
        }
        "ProfileType_NotLike" {
            if ($Device.ProfileType -NotLike $Target) {
                Write-Verbose "ProfileType doesn't match $Target"
                $ConditionMet = $true
            }
        }
        "SystemLabels_Contains" {
            if ($Device.SystemLabels -contains $Target) {
                Write-Verbose "SystemLabels contains $Target"
                $ConditionMet = $true
            }
        }
        "SystemLabels_NotContains" {
            if (-not ($Device.SystemLabels -contains $Target)) {
                Write-Verbose "SystemLabels doesn't contain $Target"
                $ConditionMet = $true
            }
        }
        "TrustType_Like" {
            if ($Device.TrustType -Like $Target) {
                Write-Verbose "TrustType matches $Target"
                $ConditionMet = $true
            }
        }
        "TrustType_NotLike" {
            if ($Device.TrustType -NotLike $Target) {
                Write-Verbose "TrustType doesn't match $Target"
                $ConditionMet = $true
            }
        }
        default {
            Write-Warning "Unknown condition type: $ConditionsType"
        }
    }

    return $ConditionMet, $ModifiedTagValue
}



function ConvertTo-HashTable() {
<#
 .Synopsis
  Convert PSCustomObject to HashTable
 .Description
  Convert PSCustomObject to HashTable
 .Example
  Get-Content "test.json" | ConvertFrom-Json | ConvertTo-HashTable
#>

    [CmdletBinding()]
    Param(
        [parameter(ValueFromPipeline)]
        $object,
        [switch] $recurse
    )
    $ht = @{}
    if ($object -is [System.Collections.Specialized.OrderedDictionary] -or $object -is [hashtable]) {
        $object.Keys | ForEach-Object {
            if ($recurse -and ($object."$_" -is [System.Collections.Specialized.OrderedDictionary] -or $object."$_" -is [hashtable] -or $object."$_" -is [PSCustomObject])) {
                $ht[$_] = ConvertTo-HashTable $object."$_" -recurse
            }
            else {
                $ht[$_] = $object."$_"
            }
        }
    }
    elseif ($object -is [PSCustomObject]) {
        $object.PSObject.Properties | ForEach-Object {
            if ($recurse -and ($_.Value -is [System.Collections.Specialized.OrderedDictionary] -or $_.Value -is [hashtable] -or $_.Value -is [PSCustomObject])) {
                $ht[$_.Name] = ConvertTo-HashTable $_.Value -recurse
            }
            else {
                $ht[$_.Name] = $_.Value
            }
        }
    }
    $ht
}



function ConvertTo-HashTable() {
<#
 .Synopsis
  Convert PSCustomObject to HashTable
 .Description
  Convert PSCustomObject to HashTable
 .Example
  Get-Content "test.json" | ConvertFrom-Json | ConvertTo-HashTable
#>

    [CmdletBinding()]
    Param(
        [parameter(ValueFromPipeline)]
        $object,
        [switch] $recurse
    )
    $ht = @{}
    if ($object -is [System.Collections.Specialized.OrderedDictionary] -or $object -is [hashtable]) {
        $object.Keys | ForEach-Object {
            if ($recurse -and ($object."$_" -is [System.Collections.Specialized.OrderedDictionary] -or $object."$_" -is [hashtable] -or $object."$_" -is [PSCustomObject])) {
                $ht[$_] = ConvertTo-HashTable $object."$_" -recurse
            }
            else {
                $ht[$_] = $object."$_"
            }
        }
    }
    elseif ($object -is [PSCustomObject]) {
        $object.PSObject.Properties | ForEach-Object {
            if ($recurse -and ($_.Value -is [System.Collections.Specialized.OrderedDictionary] -or $_.Value -is [hashtable] -or $_.Value -is [PSCustomObject])) {
                $ht[$_.Name] = ConvertTo-HashTable $_.Value -recurse
            }
            else {
                $ht[$_.Name] = $_.Value
            }
        }
    }
    $ht
}



Function Create_GMSA_Account



Function Create_GMSA_Group_AD



Function Create_GMSA_OU



function EntraAuthenticationStrength {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$PolicyName,
        
        [Parameter()]
        [string]$Description = "",  # Default to an empty string if not provided
        
        [Parameter()]
        [ValidateSet("MFA", "windowsHelloForBusiness", "fido2", "temporaryAccessPassOneTime")]
        [array]$AllowedCombinations,
        
        [Parameter()]
        [string[]]$CombinationConfigurations,
        
        [Parameter()]
        [ValidateSet("custom")]
        [string]$PolicyType,

        [Parameter()]
        [ValidateSet("mfa")]
        [string]$RequirementsSatisfied,

        [Parameter()]
        [switch]$ViewOnly,
        
        [Parameter()]
        [switch]$CreateOnly,
        
        [Parameter()]
        [switch]$ForceUpdate
    )

# Get all existing authentication strength policies
$ExistingPolicies = Get-MgPolicyAuthenticationStrengthPolicy

# Check if the policy already exists
$ExistingPolicy = $ExistingPolicies | Where-Object { $_.displayName -eq $PolicyName }

if ($ViewOnly) {
    return $ExistingPolicy
}

# Building the policy parameters hashtable
    $PolicyParams = @{
        displayName = $PolicyName
    }

    if ($PSBoundParameters.ContainsKey('Description')) {
        $PolicyParams.description = $Description
    }

    if ($PSBoundParameters.ContainsKey('RequirementsSatisfied')) {
        $PolicyParams.requirementsSatisfied = $RequirementsSatisfied
    }

    if ($PSBoundParameters.ContainsKey('AllowedCombinations')) {
        $PolicyParams.allowedCombinations = $AllowedCombinations
    }

    if ($PSBoundParameters.ContainsKey('CombinationConfigurations')) {
        $PolicyParams.combinationConfigurations = $CombinationConfigurations
    }

    if ($ExistingPolicy) {
        if ($ForceUpdate) {
            Write-Host "Updating existing authentication strength policy: $PolicyName"
            Update-MgPolicyAuthenticationStrengthPolicy -AuthenticationStrengthPolicyId $ExistingPolicy.id -BodyParameter $PolicyParams
        } else {
            Write-Host "Policy already exists. Use -ForceUpdate to modify it."
        }
    } elseif ($CreateOnly) {
        Write-Host "Creating new authentication strength policy: $PolicyName"
        New-MgPolicyAuthenticationStrengthPolicy -BodyParameter $PolicyParams
    }
}



Function EntraCAPolicy



Function EntraGroup {
    [CmdletBinding()]
    param(
        [Parameter()]
        [hashtable]$EntraGroupsHashTable,
        [Parameter(Mandatory)]
        [string]$DisplayName,
        [Parameter()]
        [AllowEmptyString()]
        [AllowNull()]
        [string]$Description,
        [Parameter()]
        [ValidateSet("Assigned", "DynamicMembership")]
        [string]$GroupType,
        [Parameter()]
        [switch]$AutomaticMailNickname,
        [Parameter()]
        [string]$MailNickname,
        [Parameter()]
        [string]$GroupQuery,
        [Parameter()]
        [boolean]$MailEnabled,
        [Parameter()]
        [boolean]$SecurityEnabled,
        [Parameter()]
        [string]$AdministrativeUnitPlacement,
        [Parameter()]
        [ValidateSet("On", "Paused")]
        [string]$MembershipRuleProcessingState,
        [Parameter()]
        [switch]$CreateOnly,
        [Parameter()]
        [switch]$ForceUpdate
    )

    # Retrieve all Entra Groups and populate hash table if not provided
    if (-not $EntraGroupsHashTable) {
        $EntraGroupsHashTable = [ordered]@{}
        Get-MgGroup -All | ForEach-Object { $EntraGroupsHashTable[$_.DisplayName] = $_ }
    }

    if ($EntraGroupsHashTable.Count -eq 0) {
        Write-host ""
        Write-Host "No Entra Groups found."
        Write-host ""
        return
    }

    # Build hash for group properties
    $CmdToRun_Hash = @{}
    if ($PSBoundParameters.ContainsKey('Description')) { $CmdToRun_Hash['description'] = $Description }
    if ($PSBoundParameters.ContainsKey('DisplayName')) { $CmdToRun_Hash['displayName'] = $DisplayName }
    if ($GroupType -eq "DynamicMembership") { $CmdToRun_Hash['GroupType'] = $GroupType }
    if ($PSBoundParameters.ContainsKey('GroupQuery')) { $CmdToRun_Hash['MembershipRule'] = $GroupQuery }
    if ($PSBoundParameters.ContainsKey('MailEnabled')) { $CmdToRun_Hash['MailEnabled'] = $MailEnabled }
    if ($PSBoundParameters.ContainsKey('SecurityEnabled')) { $CmdToRun_Hash['SecurityEnabled'] = $SecurityEnabled }
    if ($PSBoundParameters.ContainsKey('MembershipRuleProcessingState')) { $CmdToRun_Hash['membershipRuleProcessingState'] = $MembershipRuleProcessingState }

    # Handle MailNickname
    if ($PSBoundParameters.ContainsKey('MailNickname') -and -not $AutomaticMailNickname) {
        $MailNickname = $MailNickname.Replace(" ", "")
        if ($MailNickname.Length -gt 64) {
            $MailNickname = $MailNickname.Substring(0, 50) + (Get-Random -Minimum 100000 -Maximum 10000000)
        }
        $CmdToRun_Hash['MailNickname'] = $MailNickname
    }

    if ($AutomaticMailNickname) {
        $MailNickname = $DisplayName.Replace(" ", "")
        if ($MailNickname.Length -gt 64) {
            $MailNickname = $MailNickname.Substring(0, 50) + (Get-Random -Minimum 100000 -Maximum 10000000)
        }
        $CmdToRun_Hash['MailNickname'] = $MailNickname
    }

    # Check if group exists and handle accordingly
    if ($EntraGroupsHashTable.ContainsKey($DisplayName)) {
        $GroupExist = $EntraGroupsHashTable[$DisplayName]

        if ($PSBoundParameters.ContainsKey('ForceUpdate')) {
            $CmdToRun_Hash['GroupId'] = $GroupExist.Id
            Write-host "----------------------------"
            Write-Host "Updating group: $DisplayName"
            Write-host ""
            try {
                $Result = Update-MgGroup @CmdToRun_Hash
            } catch {
                $Result = New-MgGroup @CmdToRun_Hash
            }
        } else {
            Write-host "----------------------------"
            Write-Host "Group already exists: $DisplayName"
            Write-host ""
            $Result = $GroupExist
        }
    } elseif ( ($PSBoundParameters.ContainsKey('CreateOnly')) -or (-not ($EntraGroupsHashTable.ContainsKey($DisplayName)))) {
        Write-host "----------------------------"
        Write-Host "Creating group: $DisplayName"
        Write-host ""
        try {
            $Result = New-MgGroup @CmdToRun_Hash
            Write-host ""
            Write-Host "Group created successfully."
            Write-host ""
        } catch {
            Write-host ""
            Write-Host "Error creating group: $_"
            Write-host ""
        }
    } else {
        Write-host ""
        Write-Host "Group does not exist. Use -Create to create a new group."
        Write-host ""
        return
    }

    # Perform a final check to ensure the group was created/updated correctly
    Write-host ""
    Write-Host "Verifying the group with DisplayName: $DisplayName"
    Write-host ""
    $finalCheckGroups = Get-MgGroup -Filter "displayName eq '$DisplayName'"

    # Check for multiple records
    if ($finalCheckGroups.Count -gt 1) {
        throw "More than one group found with DisplayName: $DisplayName"
        break
    }

    # Return the result of the final check
    Write-host ""
    return $finalCheckGroups
}



Function EntraGroupsAsHashtable {
    $Entra_ID_Groups_ALL = Get-MgGroup -All

    # order Groups into hash
    $EntraGroupsHashTable = [ordered]@{}
    $Entra_ID_Groups_ALL | ForEach-Object { $EntraGroupsHashTable.add($_.DisplayName,$_) }
    Return $EntraGroupsHashTable
}



Function EntraNamedLocation {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]$DisplayName,
        [Parameter()]
        [AllowEmptyString()]
        [AllowNull()]
        [array]$ip4Range,
        [Parameter()]
        [AllowEmptyString()]
        [AllowNull()]
        [array]$ip6Range,
        [Parameter()]
        [AllowEmptyString()]
        [AllowNull()]
        [array]$countriesAndRegions,
        [Parameter()]
        [switch]$countryNamedLocation,
        [Parameter()]
        [switch]$ipNamedLocation,
        [Parameter()]
        [boolean]$includeUnknownCountriesAndRegions,
        [Parameter()]
        [switch]$ListALL,
        [Parameter()]
        [switch]$AppendExisting,
        [Parameter()]
        [boolean]$isTrusted,
        [Parameter()]
        [string]$countryLookupMethod,
        [Parameter()]
        [switch]$Create,
        [Parameter()]
        [switch]$ForceUpdate
    )

    # Get all Entra Named Locations
    $Entra_Named_Locations_ALL = Get-MgIdentityConditionalAccessNamedLocation
    If (($Entra_Named_Locations_ALL) -and ($PSBoundParameters.ContainsKey('ListALL'))) {
        Return $Entra_Named_Locations_ALL
    }

    If ($DisplayName) {
        # Check if Named Location exists
        $Named_Location = $Entra_Named_Locations_ALL | Where-Object { $_.displayName -eq $DisplayName }

        # countryNamedLocation
        If ($PSBoundParameters.ContainsKey('countryNamedLocation')) {
            If ($countriesAndRegions) {
                If ($PSBoundParameters.ContainsKey('AppendExisting')) {
                    $NewcountriesAndRegions = @()
                    $NewcountriesAndRegions += $Named_Location.countriesAndRegions
                    $NewcountriesAndRegions += $countriesAndRegions
                    # Remove duplicates
                    $NewcountriesAndRegions = $NewcountriesAndRegions | Sort-Object -Unique
                } Else {
                    $NewcountriesAndRegions = @()
                    $NewcountriesAndRegions += $countriesAndRegions
                }

                $Params = @{
                    '@odata.type' = '#microsoft.graph.countryNamedLocation'
                    displayName = $DisplayName
                    isTrusted = $isTrusted
                    countriesAndRegions = $NewcountriesAndRegions
                    includeUnknownCountriesAndRegions = $includeUnknownCountriesAndRegions
                    countryLookupMethod = $countryLookupMethod
                }
            } Else {
                Write-host ""
                Write-Host "Syntax error countryNamedLocation. You need to define a list in two-letter format specified by ISO 3166-2"
                Write-host ""
                Break
            }
        }

        # ipNamedLocation
        If ($PSBoundParameters.ContainsKey('ipNamedLocation')) {
            If (($ip4Range) -or ($ip6Range)) {
                If ($PSBoundParameters.ContainsKey('AppendExisting')) {
                    $Newip4Range = @()
                    $Newip4Range += $Named_Location.ipRanges | Where-Object { $_.odata.type -eq '#microsoft.graph.iPv4CidrRange' }
                    $Newip4Range += $ip4Range

                    $Newip6Range = @()
                    $Newip6Range += $Named_Location.ipRanges | Where-Object { $_.odata.type -eq '#microsoft.graph.iPv6CidrRange' }
                    $Newip6Range += $ip6Range

                    # Remove duplicates
                    $Newip4Range = $Newip4Range | Sort-Object -Unique
                    $Newip6Range = $Newip6Range | Sort-Object -Unique
                } Else {
                    $Newip4Range = $ip4Range | Sort-Object -Unique
                    $Newip6Range = $ip6Range | Sort-Object -Unique
                }

                $Params = @{
                    '@odata.type' = '#microsoft.graph.ipNamedLocation'
                    displayName = $DisplayName
                    isTrusted = $isTrusted
                    ipRanges = @()
                }

                If ($Newip4Range) {
                    $Params.ipRanges += $Newip4Range | ForEach-Object {
                        @{
                            '@odata.type' = '#microsoft.graph.iPv4CidrRange'
                            cidrAddress = $_
                        }
                    }
                }

                If ($Newip6Range) {
                    $Params.ipRanges += $Newip6Range | ForEach-Object {
                        @{
                            '@odata.type' = '#microsoft.graph.iPv6CidrRange'
                            cidrAddress = $_
                        }
                    }
                }
            } Else {
                Write-host ""
                Write-Host "Syntax error ipNamedLocation. You need to define a list in IPv4 CIDR format (e.g., 1.2.3.4/32) or any allowable IPv6 format from IETF RFC596"
                Write-host ""
                Break
            }
        }

        If ($Named_Location) { # found -> Update or View existing
            If ($ForceUpdate) {
                Write-host ""
                Write-Host "Updating Named Location"
                Write-host ""
                Update-MgIdentityConditionalAccessNamedLocation -NamedLocationId $Named_Location.id -BodyParameter $Params
            } Else {
                Return $Named_Location
            }
        } Else {
            If ($Create) {
                Write-host ""
                Write-Host "Creating Named Location"
                Write-host ""
                New-MgIdentityConditionalAccessNamedLocation -BodyParameter $Params
            } Else {
                Write-host ""
                Write-Host "Named Location does not exist. Use -Create to create a new named location."
                Write-host ""
            }
        }
    } Else {
        Write-host ""
        Write-Host "DisplayName is required to create, update, or view a named location."
        Write-host ""
    }

    # Return Parameters
    $NamedLocation = Get-MgIdentityConditionalAccessNamedLocation -Filter "displayName eq '$displayName'"
    Return $NamedLocation
}



Function EntraUser {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]$DisplayName,
        [Parameter()]
        [string]$UserPrincipalName,
        [Parameter()]
        [string]$MailNickname,
        [Parameter()]
        [string]$Password,
        [Parameter()]
        [string]$GivenName,
        [Parameter()]
        [string]$Surname,
        [Parameter()]
        [string]$JobTitle,
        [Parameter()]
        [string]$Department,
        [Parameter()]
        [string]$MobilePhone,
        [Parameter()]
        [string]$OfficeLocation,
        [Parameter()]
        [switch]$Create,
        [Parameter()]
        [switch]$ForceUpdate
    )

    # Function to get user by UserPrincipalName
    Function Get-MgUserByUPN {
        param (
            [Parameter(Mandatory)]
            [string]$UPN
        )
        try {
            $User = Get-MgUser -Filter "userPrincipalName eq '$UPN'"
            return $User
        } catch {
            return $null
        }
    }

    # Function to get user by DisplayName
    Function Get-MgUserByDisplayName {
        param (
            [Parameter(Mandatory)]
            [string]$DisplayName
        )
        try {
            $User = Get-MgUser -Filter "displayName eq '$DisplayName'"
            return $User
        } catch {
            return $null
        }
    }

    # Check if the user already exists
    $ExistingUserByUPN = if ($UserPrincipalName) { Get-MgUserByUPN -UPN $UserPrincipalName } else { $null }
    $ExistingUserByDisplayName = if ($DisplayName) { Get-MgUserByDisplayName -DisplayName $DisplayName } else { $null }

    $ExistingUser = if ($null -ne $ExistingUserByUPN) { $ExistingUserByUPN } elseif ($null -ne $ExistingUserByDisplayName) { $ExistingUserByDisplayName } else { $null }

    if ($null -ne $ExistingUser) {
        if ($ForceUpdate) {
            # Update existing user
            Write-host ""
            Write-Host "Updating user: $DisplayName"
            Write-host ""
            $UpdateParams = @{}
            if ($PSBoundParameters.ContainsKey('DisplayName')) { $UpdateParams.displayName = $DisplayName }
            if ($PSBoundParameters.ContainsKey('MailNickname')) { $UpdateParams.mailNickname = $MailNickname }
            if ($PSBoundParameters.ContainsKey('GivenName')) { $UpdateParams.givenName = $GivenName }
            if ($PSBoundParameters.ContainsKey('Surname')) { $UpdateParams.surname = $Surname }
            if ($PSBoundParameters.ContainsKey('JobTitle')) { $UpdateParams.jobTitle = $JobTitle }
            if ($PSBoundParameters.ContainsKey('Department')) { $UpdateParams.department = $Department }
            if ($PSBoundParameters.ContainsKey('MobilePhone')) { $UpdateParams.mobilePhone = $MobilePhone }
            if ($PSBoundParameters.ContainsKey('OfficeLocation')) { $UpdateParams.officeLocation = $OfficeLocation }

            try {
                Update-MgUser -UserId $ExistingUser.id -BodyParameter $UpdateParams
                Write-host ""
                Write-Host "User updated successfully."
                Write-host ""
                return $ExistingUser
            } catch {
                Write-host ""
                Write-Host "Error updating user: $_"
                Write-host ""
            }
        } else {
            Write-Host "User already exists: $($ExistingUser.displayName)"
            return $ExistingUser
        }
    } else {
        if ($Create) {
            # Ensure DisplayName and UserPrincipalName are provided for creation
            if (-not $DisplayName -or -not $UserPrincipalName) {
                Write-Host "DisplayName and UserPrincipalName are required to create a new user."
                Write-host ""
                return
            }

            # Create new user
            Write-host ""
            Write-Host "Creating new user: $DisplayName"
            $UserParams = @{
                accountEnabled = $true
                displayName = $DisplayName
                userPrincipalName = $UserPrincipalName
            }

            if ($PSBoundParameters.ContainsKey('MailNickname')) { $UserParams.mailNickname = $MailNickname }
            if ($PSBoundParameters.ContainsKey('Password')) {
                $UserParams.passwordProfile = @{
                    forceChangePasswordNextSignIn = $true
                    password = $Password
                }
            }
            if ($PSBoundParameters.ContainsKey('GivenName')) { $UserParams.givenName = $GivenName }
            if ($PSBoundParameters.ContainsKey('Surname')) { $UserParams.surname = $Surname }
            if ($PSBoundParameters.ContainsKey('JobTitle')) { $UserParams.jobTitle = $JobTitle }
            if ($PSBoundParameters.ContainsKey('Department')) { $UserParams.department = $Department }
            if ($PSBoundParameters.ContainsKey('MobilePhone')) { $UserParams.mobilePhone = $MobilePhone }
            if ($PSBoundParameters.ContainsKey('OfficeLocation')) { $UserParams.officeLocation = $OfficeLocation }

            try {
                $NewUser = New-MgUser -BodyParameter $UserParams
                Write-host ""
                Write-Host "User created successfully: $($NewUser.id)"
                Write-host ""
                return $NewUser
            } catch {
                Write-host ""
                Write-Host "Error creating user: $_"
                Write-host ""
            }
        } else {
            Write-Host "User does not exist. Use -Create to create a new user."
            Write-host ""
        }
    }
}



# Function to generate a random alphanumeric string of specified length
function Generate-RandomString {
    param (
        [int]$Length = 20
    )
    $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
    $randomString = -join ((1..$Length) | ForEach-Object { $chars[(Get-Random -Minimum 0 -Maximum $chars.Length)] })
    return $randomString
}


Function Generate-SecurePassword {
    param (
        [int]$length = 16
    )
    # Define the characters to use in the password
    $characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+[]{}|;:,.<>?'
    # Generate the password
    $password = -join ((1..$length) | ForEach-Object { $characters[(Get-Random -Maximum $characters.Length)] })
    return $password
}


function Generate-RandomString {
    param (
        [int]$Length = 20
    )
    $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
    $randomString = -join ((1..$Length) | ForEach-Object { $chars[(Get-Random -Minimum 0 -Maximum $chars.Length)] })
    return $randomString
}



function Get-MgGroupMemberRecurse 
{
    param(
            [Parameter()]
                [string]$GroupUPN,
            [Parameter()]
                [string]$GroupId
        )
 
    $Members = @()
    
    if ($GroupUPN)
        {
            # find group
            $Group = Get-MgGroup -Filter "startsWith(userPrincipalName, $GroupUPN)"
        }
    ElseIf ($GroupId)
        {
            # find group
            $Group = Get-MgGroup -Filter "id eq '$GroupId'"
        }

        If ($Group)
            {
                $GroupMembers = Get-MgGroupMember -GroupId $Group.Id | select * -ExpandProperty additionalProperties | Select-Object @(
                    'id'
                    @{  Name       = 'userPrincipalName'
                        Expression = { $_.AdditionalProperties["userPrincipalName"] }
                    }
                    @{  Name       = 'type'
                        Expression = { $_.AdditionalProperties["@odata.type"] }
                    }
                )

                If ($GroupMembers)
                    {
                        ForEach ($Member in $GroupMembers)
                            {
                                if ($Member.type -eq "#microsoft.graph.user") {
                                    $Members += $Member
                                }
                                if ($Member.type -eq "#microsoft.graph.group") {
                                    $Members += @(Get-MgGroupMemberRecurse -GroupUPN $_.userPrincipalName)
                                }
                            }
                    }
            }
return $Members
}


function Get-MgGroupMemberRecurse 



Function Install_GMSA_Account



Function Invoke-ADSDPropagation
{
<#
.SYNOPSIS
    Invoke a SDProp task on the PDCe.
.DESCRIPTION
    Make an LDAP call to trigger SDProp.
.EXAMPLE
    Invoke-ADSDPropagation
    By default, RunProtectAdminGroupsTask is used.
.EXAMPLE
    Invoke-ADSDPropagation -TaskName FixUpInheritance
    Use the legacy FixUpInheritance task name for Windows Server 2003 and earlier.
.PARAMETER TaskName
    Name of the task to use.
        - FixUpInheritance for legacy OS
        - RunProtectAdminGroupsTask for recent OS
.NOTES
    You can track progress with:
    Get-Counter -Counter '\directoryservices(ntds)\ds security descriptor propagator runtime queue' | Select-Object -ExpandProperty CounterSamples | Select-Object -ExpandProperty CookedValue
.LINK
    http://ItForDummies.net
#>

[CmdletBinding()]
Param(
    [Parameter(Mandatory=$false,
        HelpMessage='Name of the domain where to force SDProp to run',
        Position=0)]
    [ValidateScript({Test-Connection -ComputerName $_ -Count 2 -Quiet})]
    [String]$DomainName = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain().Name,

    [ValidateSet('RunProtectAdminGroupsTask','FixUpInheritance')]
    [String]$TaskName = 'RunProtectAdminGroupsTask'
)

    try
        {
            $DomainContext = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext('domain',$DomainName)
            $DomainObject = [System.DirectoryServices.ActiveDirectory.Domain]::GetDomain($DomainContext)
    
            Write-Verbose -Message "Detected PDCe is $($DomainObject.PdcRoleOwner.Name)."
            $RootDSE = New-Object System.DirectoryServices.DirectoryEntry("LDAP://$($DomainObject.PdcRoleOwner.Name)/RootDSE") 
            $RootDSE.UsePropertyCache = $false 
            $RootDSE.Put($TaskName, "1") # RunProtectAdminGroupsTask & fixupinheritance
            $RootDSE.SetInfo()
        }
    catch
        {
            throw "Can't invoke SDProp on $($DomainObject.PdcRoleOwner.Name) !"
        }
}



 Function Invoke-ADSDPropagation



Function TagDeviceConditionsTrue {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [object]$Device,
        [Parameter(Mandatory)] [string]$PropertyKeyAD,
        [Parameter(Mandatory)] [string]$PropertyKeyCloud,
        [Parameter(Mandatory)] [AllowEmptyString()] [AllowNull()] [string]$TagValueAD,
        [Parameter(Mandatory)] [AllowEmptyString()] [AllowNull()] [string]$TagValueCloud,
        [Parameter()] [AllowNull()] [string]$OnPremisesSyncEnabled
    )

    # Get existing tag-values
    $ExistingTagValue = $Device.ExtensionAttributes.$PropertyKeyCloud

    if ($TagValueAD -eq "") { $TagValueAD = $null }
    if ($TagValueCloud -eq "") { $TagValueCloud = $null }

    # Cloud-only device
    if ([string]::IsNullOrEmpty($OnPremisesSyncEnabled)) {
        Write-Verbose "PropertyKeyCloud : $PropertyKeyCloud"
        Write-Verbose "ExistingValue : $ExistingTagValue"
        Write-Verbose "TagValueCloud : $TagValueCloud"

        if ($ExistingTagValue -eq $TagValueCloud) {
            Write-Verbose "Skipping as value is already set correctly on device."
        } else {
            if (-not $global:EnableWhatIf) {
                Write-Host "Modifying device $($Device.DisplayName) in Microsoft Graph ($PropertyKeyCloud = $TagValueCloud)"

                try {
                    Update-MgBetaDevice -DeviceId $Device.Id -ExtensionAttributes @{"$PropertyKeyCloud"="$TagValueCloud"} -ErrorAction Stop
                } catch {
                    Write-Warning "Failed to update device $($Device.DisplayName) in Graph."
                }

                $LogEntry = [PSCustomObject]@{ 
                    DeviceName = $Device.DisplayName
                    DeviceId = $Device.Id
                    PropertyKeyAD = $PropertyKeyAD
                    TagValueAD = $TagValueAD
                    PropertyKeyCloud = $PropertyKeyCloud
                    TagValueCloud = $TagValueCloud
                    ExistingTagValue = $ExistingTagValue
                }
                $Result = $Global:ModificationsLog.Add($LogEntry)
            } else {
                Write-Host "WhatIf - Modifying device $($Device.DisplayName) ($PropertyKeyCloud = $TagValueCloud)"

                $LogEntry = [PSCustomObject]@{ 
                    DeviceName = $Device.DisplayName
                    DeviceId = $Device.Id
                    PropertyKeyAD = $PropertyKeyAD
                    TagValueAD = $TagValueAD
                    PropertyKeyCloud = $PropertyKeyCloud
                    TagValueCloud = $TagValueCloud
                    ExistingTagValue = $ExistingTagValue
                }
                $Result = $Global:ModificationsLog.Add($LogEntry)
            }
        }
    } else {
        # Device is Hybrid AD joined (on-prem sync enabled)
        Write-Verbose "PropertyKeyAD : $PropertyKeyAD"
        Write-Verbose "ExistingValue : $ExistingTagValue"
        Write-Verbose "TagValueAD : $TagValueAD"

        if ($ExistingTagValue -eq $TagValueAD) {
            Write-Verbose "Skipping as value is already set correctly on device."
        } else {
            if (-not $global:EnableWhatIf) {
                Write-Host "Modifying device $($Device.DisplayName) in Active Directory ($PropertyKeyAD = $TagValueAD)"

                $DeviceAD = Get-ADComputer -Filter { Name -eq $Device.DisplayName }
                try {
                    if ($global:SecureCredentials) {
                        Set-ADComputer -Identity $DeviceAD -Replace @{"$PropertyKeyAD"="$TagValueAD"} -Credential $global:SecureCredentials
                    } else {
                        Set-ADComputer -Identity $DeviceAD -Replace @{"$PropertyKeyAD"="$TagValueAD"}
                    }
                } catch {
                    try {
                        if ($global:SecureCredentials) {
                            Set-ADComputer -Identity $DeviceAD -Add @{"$PropertyKeyAD"="$TagValueAD"} -Credential $global:SecureCredentials
                        } else {
                            Set-ADComputer -Identity $DeviceAD -Add @{"$PropertyKeyAD"="$TagValueAD"}
                        }
                    } catch {
                        Write-Warning "Failed to modify device $($Device.DisplayName) in AD."
                    }
                }

                $LogEntry = [PSCustomObject]@{ 
                    DeviceName = $Device.DisplayName
                    DeviceId = $Device.Id
                    PropertyKeyAD = $PropertyKeyAD
                    TagValueAD = $TagValueAD
                    PropertyKeyCloud = $PropertyKeyCloud
                    TagValueCloud = $TagValueCloud
                    ExistingTagValue = $ExistingTagValue
                }
                $Result = $Global:ModificationsLog.Add($LogEntry)
            } else {
                Write-Host "WhatIf - Modifying device $($Device.DisplayName) in AD ($PropertyKeyAD = $TagValueAD)"

                $LogEntry = [PSCustomObject]@{ 
                    DeviceName = $Device.DisplayName
                    DeviceId = $Device.Id
                    PropertyKeyAD = $PropertyKeyAD
                    TagValueAD = $TagValueAD
                    PropertyKeyCloud = $PropertyKeyCloud
                    TagValueCloud = $TagValueCloud
                    ExistingTagValue = $ExistingTagValue
                }
                $Result = $Global:ModificationsLog.Add($LogEntry)
            }
        }
    }
}



Function TagUser



Function TagUserConditionsTrue {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [object]$User,
        [Parameter(Mandatory)] [string]$PropertyKeyAD,
        [Parameter(Mandatory)] [string]$PropertyKeyCloud,
        [Parameter(Mandatory)] [string]$TagValueAD,
        [Parameter(Mandatory)] [string]$TagValueCloud,
        [Parameter()] [AllowNull()] [string]$OnPremisesSyncEnabled
    )

    # Get existing tag-values
    $ExistingTagValue = $User.OnPremisesExtensionAttributes.$PropertyKeyCloud

    # Cloud-only Account (use Microsoft Graph to update)
    if ([string]::IsNullOrEmpty($OnPremisesSyncEnabled)) {
        write-verbose ""
        write-Verbose "PropertyKeyCloud : $($PropertyKeyCloud)"
        write-Verbose "ExistingValue : $($ExistingTagValue)"
        write-Verbose "TagValueCloud : $($TagValueCloud)"

        if ($ExistingTagValue -eq $TagValueCloud) {
        write-verbose ""
        write-Verbose "Skipping as value is already set correctly on user !!!"
                
        } elseIf ($ExistingTagValue -ne $TagValueCloud) {
            if (-not $global:EnableWhatIf) {
                Write-Host ""
                Write-Host " Modifying $($User.DisplayName) using Microsoft Graph ($($PropertyKeyCloud) = $($TagValueCloud))"

                try {
                    Update-MgBetaUser -UserId $User.Id -OnPremisesExtensionAttributes @{"$($PropertyKeyCloud)"="$($TagValueCloud)"} -ErrorAction Stop
                } catch {
                    Write-Host ""
                    Write-Host " Modifying $($User.DisplayName) using Exchange Online ($($PropertyKeyCloud) = $($TagValueCloud))"
                            
                    # Handle updates via Exchange Online cmdlets
                    switch ($PropertyKeyCloud) {
                        'extensionAttribute1'  { set-mailuser -identity $User.UserPrincipalName -CustomAttribute1 $TagValueCloud -WarningAction SilentlyContinue; set-mailbox -identity $User.UserPrincipalName -CustomAttribute1 $TagValueCloud -WarningAction SilentlyContinue }
                        'extensionAttribute2'  { set-mailuser -identity $User.UserPrincipalName -CustomAttribute2 $TagValueCloud -WarningAction SilentlyContinue; set-mailbox -identity $User.UserPrincipalName -CustomAttribute2 $TagValueCloud -WarningAction SilentlyContinue }
                        'extensionAttribute3'  { set-mailuser -identity $User.UserPrincipalName -CustomAttribute3 $TagValueCloud -WarningAction SilentlyContinue; set-mailbox -identity $User.UserPrincipalName -CustomAttribute3 $TagValueCloud -WarningAction SilentlyContinue }
                        'extensionAttribute4'  { set-mailuser -identity $User.UserPrincipalName -CustomAttribute4 $TagValueCloud -WarningAction SilentlyContinue; set-mailbox -identity $User.UserPrincipalName -CustomAttribute4 $TagValueCloud -WarningAction SilentlyContinue }
                        'extensionAttribute5'  { set-mailuser -identity $User.UserPrincipalName -CustomAttribute5 $TagValueCloud -WarningAction SilentlyContinue; set-mailbox -identity $User.UserPrincipalName -CustomAttribute5 $TagValueCloud -WarningAction SilentlyContinue }
                        'extensionAttribute6'  { set-mailuser -identity $User.UserPrincipalName -CustomAttribute6 $TagValueCloud -WarningAction SilentlyContinue; set-mailbox -identity $User.UserPrincipalName -CustomAttribute6 $TagValueCloud -WarningAction SilentlyContinue }
                        'extensionAttribute7'  { set-mailuser -identity $User.UserPrincipalName -CustomAttribute7 $TagValueCloud -WarningAction SilentlyContinue; set-mailbox -identity $User.UserPrincipalName -CustomAttribute7 $TagValueCloud -WarningAction SilentlyContinue }
                        'extensionAttribute8'  { set-mailuser -identity $User.UserPrincipalName -CustomAttribute8 $TagValueCloud -WarningAction SilentlyContinue; set-mailbox -identity $User.UserPrincipalName -CustomAttribute8 $TagValueCloud -WarningAction SilentlyContinue }
                        'extensionAttribute9'  { set-mailuser -identity $User.UserPrincipalName -CustomAttribute9 $TagValueCloud -WarningAction SilentlyContinue; set-mailbox -identity $User.UserPrincipalName -CustomAttribute9 $TagValueCloud -WarningAction SilentlyContinue }
                        'extensionAttribute10' { set-mailuser -identity $User.UserPrincipalName -CustomAttribute10 $TagValueCloud -WarningAction SilentlyContinue; set-mailbox -identity $User.UserPrincipalName -CustomAttribute10 $TagValueCloud -WarningAction SilentlyContinue }
                        'extensionAttribute11' { set-mailuser -identity $User.UserPrincipalName -CustomAttribute11 $TagValueCloud -WarningAction SilentlyContinue; set-mailbox -identity $User.UserPrincipalName -CustomAttribute11 $TagValueCloud -WarningAction SilentlyContinue }
                        'extensionAttribute12' { set-mailuser -identity $User.UserPrincipalName -CustomAttribute12 $TagValueCloud -WarningAction SilentlyContinue; set-mailbox -identity $User.UserPrincipalName -CustomAttribute12 $TagValueCloud -WarningAction SilentlyContinue }
                        'extensionAttribute13' { set-mailuser -identity $User.UserPrincipalName -CustomAttribute13 $TagValueCloud -WarningAction SilentlyContinue; set-mailbox -identity $User.UserPrincipalName -CustomAttribute13 $TagValueCloud -WarningAction SilentlyContinue }
                        'extensionAttribute14' { set-mailuser -identity $User.UserPrincipalName -CustomAttribute14 $TagValueCloud -WarningAction SilentlyContinue; set-mailbox -identity $User.UserPrincipalName -CustomAttribute14 $TagValueCloud -WarningAction SilentlyContinue }
                        'extensionAttribute15' { set-mailuser -identity $User.UserPrincipalName -CustomAttribute15 $TagValueCloud -WarningAction SilentlyContinue; set-mailbox -identity $User.UserPrincipalName -CustomAttribute15 $TagValueCloud -WarningAction SilentlyContinue }
                    }
                }

                # Log entry
                $LogEntry = [PSCustomObject]@{ 
                    UserUPN = $User.UserPrincipalName
                    UserDisplayName = $User.DisplayName
                    OnPremisesSyncEnabled = [string]$OnPremisesSyncEnabled
                    PropertyKeyAD = $PropertyKeyAD
                    TagValueAD = $TagValueAD
                    PropertyKeyCloud = $PropertyKeyCloud
                    TagValueCloud = $TagValueCloud
                    ExistingTagValue = $ExistingTagValue
                }

                $Result = $Global:ModificationsLog.add($LogEntry) 
            } else {
                Write-Host ""
                Write-Host " WhatIf - Modifying $($User.DisplayName) using Microsoft Graph ($($PropertyKeyCloud) = $($TagValueCloud))"

                # Log entry
                $LogEntry = [PSCustomObject]@{ 
                    UserUPN = $User.UserPrincipalName
                    UserDisplayName = $User.DisplayName
                    OnPremisesSyncEnabled = [string]$OnPremisesSyncEnabled
                    PropertyKeyAD = $PropertyKeyAD
                    TagValueAD = $TagValueAD
                    PropertyKeyCloud = $PropertyKeyCloud
                    TagValueCloud = $TagValueCloud
                    ExistingTagValue = $ExistingTagValue
                }

                $Result = $Global:ModificationsLog.add($LogEntry) 
            }
        }
            
    } elseif (-not [string]::IsNullOrEmpty($OnPremisesSyncEnabled)) {

        write-verbose ""
        write-Verbose "PropertyKeyAD : $($PropertyKeyAD)"
        write-Verbose "ExistingValue : $($ExistingTagValue)"
        write-Verbose "TagValueAD : $($TagValueAD)"
                                
        if ($ExistingTagValue -eq $TagValueAD) {
        write-verbose ""
        write-Verbose "Skipping as value is already set correctly on user !!!"
                
        } elseIf ($ExistingTagValue -ne $TagValueAD) {
            if (-not $global:EnableWhatIf) {
                Write-Host ""
                Write-Host " Modifying $($User.DisplayName) using Active Directory ($($PropertyKeyAD) = $($TagValueAD))"

                $UserAD = Get-ADUser -Filter 'UserPrincipalName -eq $User.OnPremisesUserPrincipalName'
                try {
                        If ($global:SecureCredentials) {
                            Set-ADUser -identity $UserAD -Replace @{"$PropertyKeyAD"="$($TagValueAD)"} -Credential $global:SecureCredentials
                        } Else {
                            Set-ADUser -identity $UserAD -Replace @{"$PropertyKeyAD"="$($TagValueAD)"}
                        }
                } catch {
                        If ($global:SecureCredentials) {
                            Set-ADUser -identity $UserAD -Add @{"$PropertyKeyAD"="$($TagValueAD)"} -Credential $global:SecureCredentials
                        } Else {
                            Set-ADUser -identity $UserAD -Add @{"$PropertyKeyAD"="$($TagValueAD)"}
                        }
                }

                # Log entry
                $LogEntry = [PSCustomObject]@{ 
                    UserUPN = $User.UserPrincipalName
                    UserDisplayName = $User.DisplayName
                    OnPremisesSyncEnabled = [string]$OnPremisesSyncEnabled
                    PropertyKeyAD = $PropertyKeyAD
                    TagValueAD = $TagValueAD
                    PropertyKeyCloud = $PropertyKeyCloud
                    TagValueCloud = $TagValueCloud
                    ExistingTagValue = $ExistingTagValue
                }

                $Result = $Global:ModificationsLog.add($LogEntry) 
            } else {
                Write-Host ""
                Write-Host " WhatIf - Modifying $($User.DisplayName) using Active Directory ($($PropertyKeyAD) = $($TagValueAD))"

                # Log entry
                $LogEntry = [PSCustomObject]@{ 
                    UserUPN = $User.UserPrincipalName
                    UserDisplayName = $User.DisplayName
                    OnPremisesSyncEnabled = [string]$OnPremisesSyncEnabled
                    PropertyKeyAD = $PropertyKeyAD
                    TagValueAD = $TagValueAD
                    PropertyKeyCloud = $PropertyKeyCloud
                    TagValueCloud = $TagValueCloud
                    ExistingTagValue = $ExistingTagValue
                }

                $Result = $Global:ModificationsLog.add($LogEntry) 
            }
        }
    }
}