Scripts/Merge-GHFIDOMetaData.ps1

# Load existing FIDO keys from local JSON file
$existingKeyFile = "Assets/FidoKeys.json"
$existingFIDOKeys = Get-Content $existingKeyFile -Raw | ConvertFrom-Json -Depth 10

# Fetch FIDO Alliance keys from the MDS3 endpoint
$FIDOUri = "https://mds3.fidoalliance.org/"
$FIDOAURL = Invoke-WebRequest -Uri $FIDOUri
$FIDOAKeys = $FIDOAURL | Get-JWTDetails

# Initialize the log buffer as an ArrayList
$LogBuffer = New-Object System.Collections.ArrayList

# Define the log file path
$LogFilePath = "FAmerge_log.txt"

# Initialize the changes tracking variable
$ChangesMade = $false

# Create a list of common AAGUIDs between existing keys and FIDO Alliance keys
$CommonAAGUIDs = @($existingFIDOKeys.keys.AAGUID) | Where-Object {
    $FIDOAKeys.entries.aaguid -contains $_
}

# Function to get a timestamp for logging
function Get-Timestamp {
    return (Get-Date -AsUTC).ToString("yyyy-MM-dd HH:mm:ss")
}

# Function to add log entries to the log buffer
function Add-ToLogBuffer {
    param (
        [Parameter(Mandatory = $true)]
        [string]$Value
    )
    $script:LogBuffer.Add($Value) | Out-Null
}

# Function to normalize 'versions' property
function Normalize-Versions {
    param ([PSCustomObject]$metadataStatement)
    
    if ($metadataStatement.authenticatorGetInfo) {
        $versions = $metadataStatement.authenticatorGetInfo.versions
        if ($versions) {
            $normalizedVersions = $versions | ForEach-Object {
                $_ -replace '([0-9])_([0-9])', '$1.$2' -replace '_', ' '
            }
            $metadataStatement.authenticatorGetInfo.versions = $normalizedVersions
        }
    }
    return $metadataStatement
}

# Function to ensure that specified properties are always arrays
function Ensure-ArrayProperty {
    param (
        [Parameter(Mandatory = $true)]
        [psobject]$Object,
        [Parameter(Mandatory = $true)]
        [string[]]$PropertyNames
    )

    foreach ($property in $Object.PSObject.Properties) {
        $value = $property.Value
        if ($PropertyNames -contains $property.Name) {
            if ($value -isnot [System.Collections.IEnumerable] -or $value -is [string]) {
                # Wrap the value in an array
                $Object.$($property.Name) = @($value)
            }
        } elseif ($value -is [PSCustomObject]) {
            Ensure-ArrayProperty -Object $value -PropertyNames $PropertyNames
        } elseif ($value -is [System.Collections.IEnumerable] -and -not ($value -is [string])) {
            foreach ($item in $value) {
                if ($item -is [PSCustomObject]) {
                    Ensure-ArrayProperty -Object $item -PropertyNames $PropertyNames
                }
            }
        }
    }
}

# Function to compare two objects and update differences
function Compare-Objects {
    param (
        [ref]$obj1,
        [ref]$obj2,
        $path = '',
        [switch]$update,
        $AAGUID
    )

    # Exclude automatic properties
    $excludedProperties = @('PSPath', 'PSParentPath', 'PSChildName', 'PSDrive', 'PSProvider', 'ReadCount', 'Length', 'Count')
    # Get properties that are part of the original JSON data
    $jsonProperties = $obj1.Value.PSObject.Properties | Where-Object {
        $_.MemberType -eq 'NoteProperty' -and
        ($excludedProperties -notcontains $_.Name)
    }

    foreach ($prop in $jsonProperties) {
        $name = $prop.Name
        $value1 = $prop.Value
        $value2 = $obj2.Value.PSObject.Properties[$name]?.Value
        $currentPath = if ($path) { "$path.$name" } else { $name }

        if ($value1 -is [PSCustomObject] -or $value1 -is [Array]) {
            if ($value1 -is [Array] -and $value2 -is [Array]) {
                if ($value1.Count -ne $value2.Count) {
                    if ($update) {
                        $timestamp = Get-Timestamp
                        $value1Str = $value1 | ConvertTo-Json -Depth 100
                        $value2Str = $value2 | ConvertTo-Json -Depth 100
                        $logMessage = @"
$timestamp - AAGUID: $AAGUID
Path: $currentPath
Old Value:
$value1Str

New Value:
$value2Str

"@

                        Write-Output $logMessage
                        Add-ToLogBuffer -Value $logMessage
                        # Set the changes made flag to true
                        $script:ChangesMade = $true
                        # Update the value
                        if ($prop.IsSettable) {
                            $obj1.Value.$name = $value2
                        }
                    }
                } else {
                    for ($i = 0; $i -lt $value1.Count; $i++) {
                        $item1 = $value1[$i]
                        $item2 = $value2[$i]
                        Compare-Objects -obj1 ([ref]$item1) -obj2 ([ref]$item2) -path "$currentPath`[$i`]" -update:$update -AAGUID $AAGUID
                    }
                }
            } else {
                Compare-Objects -obj1 ([ref]$value1) -obj2 ([ref]$value2) -path $currentPath -update:$update -AAGUID $AAGUID
            }
        } else {
            if ($value1 -ne $value2) {
                if ($update) {
                    $timestamp = Get-Timestamp
                    $value1Str = try { $value1 | ConvertTo-Json -Depth 100 } catch { $value1.ToString() }
                    $value2Str = try { $value2 | ConvertTo-Json -Depth 100 } catch { $value2.ToString() }
                    $logMessage = "$timestamp - Updating AAGUID $AAGUID : $currentPath : $value1Str -> $value2Str"
                    Write-Output $logMessage
                    Add-ToLogBuffer -Value $logMessage
                    # Set the changes made flag to true
                    $script:ChangesMade = $true
                    # Update the value
                    if ($prop.IsSettable) {
                        $obj1.Value.$name = $value2
                    }
                }
            }
        }
    }
}

# Main comparison and update loop
foreach ($AAGUID in $CommonAAGUIDs) {
    # Get the entries with the current AAGUID
    $EXXXKey = $existingFIDOKeys.keys | Where-Object { $_.AAGUID -eq $AAGUID }
    $FAAAKey = $FIDOAKeys.entries | Where-Object { $_.aaguid -eq $AAGUID }

    if ($EXXXKey -and $FAAAKey) {
        # Update timeOfLastStatusChange if different
        if ($EXXXKey.timeOfLastStatusChange -ne $FAAAKey.timeOfLastStatusChange) {
            $timestamp = Get-Timestamp
            $logMessage = @"
$timestamp - AAGUID: $AAGUID
Updating timeOfLastStatusChange
Old Value:
$($EXXXKey.timeOfLastStatusChange)

New Value:
$($FAAAKey.timeOfLastStatusChange)

"@

            Write-Output $logMessage
            Add-ToLogBuffer -Value $logMessage
            $EXXXKey.timeOfLastStatusChange = $FAAAKey.timeOfLastStatusChange
            # Set the changes made flag to true
            $script:ChangesMade = $true
        }

        # Normalize versions
        $EXXXKey.metadataStatement = Normalize-Versions $EXXXKey.metadataStatement
        $FAAAKey.metadataStatement = Normalize-Versions $FAAAKey.metadataStatement

        # Compare and update metadataStatement
        Compare-Objects -obj1 ([ref]$EXXXKey.metadataStatement) -obj2 ([ref]$FAAAKey.metadataStatement) -update -AAGUID $AAGUID

        # Compare and update statusReports
        if ($EXXXKey.statusReports -and $FAAAKey.statusReports) {
            Compare-Objects -obj1 ([ref]$EXXXKey.statusReports) -obj2 ([ref]$FAAAKey.statusReports) -path 'statusReports' -update -AAGUID $AAGUID
        }
    }
}

# Enforce that 'versions' properties are always arrays
foreach ($key in $existingFIDOKeys.keys) {
    if ($key.metadataStatement) {
        Ensure-ArrayProperty -Object $key.metadataStatement -PropertyNames 'versions'
    }
}

# Check if any changes were made
if (-not $ChangesMade) {
    $message = "No changes have been detected since last run."
    Write-Output $message
    Add-ToLogBuffer -Value $message
}

# Add the separator line to the log buffer at the top
$separator = "===== Script Run on $(Get-Date -AsUTC -Format 'yyyy-MM-dd HH:mm:ss') UTC ====="
$LogBuffer.InsertRange(0, @('', $separator))

# Prepend the collected log entries to the log file
if (Test-Path $LogFilePath) {
    $existingContent = Get-Content -Path $LogFilePath -Raw
    $LogContent = ($LogBuffer -join "`n") + "`n" + $existingContent
    Set-Content -Path $LogFilePath -Value $LogContent
} else {
    # If the file doesn't exist, create it with the log content
    $LogContent = $LogBuffer -join "`n"
    Set-Content -Path $LogFilePath -Value $LogContent
}

# Save the updated $existingFIDOKeys to the JSON file
$UpdatedKeysJson = $existingFIDOKeys | ConvertTo-Json -Depth 100
Set-Content -Path 'Assets/FidoKeys.json' -Value $UpdatedKeysJson

# Calculate counts
$existingKeyCount = $existingFIDOKeys.keys.Count
$FIDOAKeyCount = $FIDOAKeys.entries.Count

# Set outputs for GitHub Actions
$githubOutput = $env:GITHUB_OUTPUT
Add-Content -Path $githubOutput -Value "existingKeyCount=$existingKeyCount"
Add-Content -Path $githubOutput -Value "FIDOAKeyCount=$FIDOAKeyCount"
Add-Content -Path $githubOutput -Value "changesMade=$ChangesMade"