Public/Test-GSAForwardingProfileTimestamp.ps1

#
# Function: Test-GSAForwardingProfileTimestamp
# Checks the ForwardingProfileTimestamp registry value to detect stale profiles.
# The cloudcoffee.ch troubleshooting guide notes this key shows the last UTC update time;
# changes in the portal can take up to one hour to reach the client.
#
function Test-GSAForwardingProfileTimestamp
{
    <#
.SYNOPSIS
    Checks when the GSA ForwardingProfile was last retrieved from Microsoft Graph.
.DESCRIPTION
    Reads HKLM:\SOFTWARE\Microsoft\Global Secure Access Client\ForwardingProfileTimestamp
    and reports the age of the profile. A missing value means the profile was never
    downloaded. A value older than 2 hours is flagged as a warning since profile changes
    can take up to 1 hour to propagate; older than that suggests a retrieval failure.
    Fix: delete ForwardingProfileTimestamp and restart GlobalSecureAccessForwardingProfileService.
#>

    [CmdletBinding()]
    param (
        [switch]$PassThru,
        [ValidateSet("ERROR", "WARNING", "INFO", "DEBUG", "SUCCESS")]
        [string]$LogLevel
    )

    if ($PSBoundParameters.ContainsKey('LogLevel'))
    { $script:LogLevel = $LogLevel }
    $registryPath = "HKLM:\SOFTWARE\Microsoft\Global Secure Access Client"
    $valueName = "ForwardingProfileTimestamp"
    $warnAgeHours = 2

    # Begin checking the ForwardingProfileTimestamp registry value.
    Write-Log "Checking ForwardingProfileTimestamp registry value..." "INFO"
    $timestampValue = Get-ItemPropertyValue -Path $registryPath -Name $valueName -ErrorAction SilentlyContinue

    if ([string]::IsNullOrEmpty($timestampValue))
    {
        Write-Log "ForwardingProfileTimestamp is missing - the forwarding profile has never been downloaded. Ensure network connectivity to graph.microsoft.com and that the user is assigned to a forwarding profile in Entra portal." "WARNING"
        if ($PassThru) { return [PSCustomObject]@{ Passed = $true; Warnings = 1 } }
        return
    }

    # Parse the timestamp value and calculate the age of the forwarding profile.
    try
    {
        $profileTime = [datetime]::Parse($timestampValue, [System.Globalization.CultureInfo]::InvariantCulture,
            [System.Globalization.DateTimeStyles]::RoundtripKind)
        $age = (Get-Date).ToUniversalTime() - $profileTime.ToUniversalTime()
        $ageFormatted = "$([int]$age.TotalHours)h $($age.Minutes)m"

        if ($age.TotalHours -le $warnAgeHours)
        {
            Write-Log "ForwardingProfile was last retrieved $ageFormatted ago (UTC: $($profileTime.ToString('u'))) - profile is current." "SUCCESS"
            return $true
        }
        else
        {
            Write-Log "ForwardingProfile was last retrieved $ageFormatted ago (UTC: $($profileTime.ToString('u'))) - profile may be stale. To force a refresh: delete the ForwardingProfileTimestamp registry key and restart GlobalSecureAccessForwardingProfileService." "WARNING"
            if ($PassThru) { return [PSCustomObject]@{ Passed = $true; Warnings = 1 } }
            return
        }
    }
    catch
    {
        Write-Log "ForwardingProfileTimestamp value '$timestampValue' could not be parsed as a date: $($_.Exception.Message)" "WARNING"
        if ($PassThru) { return [PSCustomObject]@{ Passed = $true; Warnings = 1 } }
        return
    }
}