Functions/Private/Merge-FidoData.ps1

<#
.SYNOPSIS
Merges FIDO data from a JSON file and a URL source, updating and validating entries.

.DESCRIPTION
The `Merge-FidoData` function reads FIDO key data from a JSON file and a URL source, merges the data, validates vendors, and logs changes. It updates the JSON file with the merged data and generates log and markdown files documenting the changes.

.PARAMETER Url
The URL to fetch the FIDO key data from. Defaults to "https://learn.microsoft.com/en-us/entra/identity/authentication/concept-fido2-hardware-vendor".

.PARAMETER JsonFilePath
The file path to the JSON file containing the original FIDO key data. If not provided, a default path is constructed.

.PARAMETER LogFilePath
The file path to the log file where changes are documented. If not provided, a default path is constructed.

.PARAMETER MarkdownFilePath
The file path to the markdown file where changes are documented. If not provided, a default path is constructed.

.EXAMPLE
Merge-FidoData -JsonFilePath "C:\path\to\FidoKeys.json" -LogFilePath "C:\path\to\merge_log.txt" -MarkdownFilePath "C:\path\to\merge_log.md"

This example merges FIDO data from the specified JSON file and URL, logs changes to the specified log and markdown files, and updates the JSON file with the merged data.

.NOTES
- The function validates vendors against a predefined list and prompts for valid vendor names if necessary.
- Duplicate entries in both JSON and URL data are logged.
- The function ensures that AAGUIDs are unique in the merged data.
- Changes are logged and saved to both log and markdown files.
#>

function Merge-FidoData {
    [CmdletBinding()]
    param (
        [Parameter()]
        [string]$Url = "https://learn.microsoft.com/en-us/entra/identity/authentication/concept-fido2-hardware-vendor",

        [Parameter()]
        [string]$JsonFilePath,

        [Parameter()]
        [string]$LogFilePath,

        [Parameter()]
        [string]$MarkdownFilePath
    )
    
    # If JsonFilePath is not provided, construct the default path
    if (-not $PSBoundParameters.ContainsKey('JsonFilePath')) {
        $parentDir = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent
        $JsonFilePath = Join-Path -Path $parentDir -ChildPath "Assets/FidoKeys.json"
    }

    # If LogFilePath is not provided, construct the default path
    if (-not $PSBoundParameters.ContainsKey('LogFilePath')) {
        $parentDir = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent
        $LogFilePath = Join-Path -Path $parentDir -ChildPath "merge_log.txt"
    }

    # If MarkdownFilePath is not provided, construct the default path
    if (-not $PSBoundParameters.ContainsKey('MarkdownFilePath')) {
        $parentDir = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent
        $MarkdownFilePath = Join-Path -Path $parentDir -ChildPath "merge_log.md"
    }

    # Read the original JSON file
    if (-Not (Test-Path -Path $JsonFilePath)) {
        Write-Error "The JSON file was not found at path: $JsonFilePath"
        return
    }
    $jsonData = Get-Content -Raw -Path $JsonFilePath | ConvertFrom-Json

    # Initialize a new JSON structure based on the template
    $mergedData = @{
        metadata = @{
            databaseLastUpdated = $jsonData.metadata.databaseLastUpdated
            databaseLastChecked = (Get-Date).ToString("yyyy-MM-ddTHH:mm:ssZ")
        }
        keys = @()
    }

    # Create a hash table for quick lookup of JSON data by AAGUID
    $jsonDataByAAGUID = @{}
    $jsonDuplicates = @{}
    foreach ($jsonItem in $jsonData.keys) {
        if ($jsonDataByAAGUID.ContainsKey($jsonItem.AAGUID)) {
            $jsonDuplicates[$jsonItem.AAGUID] = $jsonItem
        } else {
            $jsonDataByAAGUID[$jsonItem.AAGUID] = $jsonItem
        }
    }

    # Create a hash table for quick lookup of URL data by AAGUID
    $urlData = Export-EntraFido -Url $Url
    $urlDataByAAGUID = @{}
    $urlDuplicates = @{}
    foreach ($urlItem in $urlData) {
        if ($urlDataByAAGUID.ContainsKey($urlItem.AAGUID)) {
            $urlDuplicates[$urlItem.AAGUID] = $urlItem
        } else {
            $urlDataByAAGUID[$urlItem.AAGUID] = $urlItem
        }
    }

    # Initialize log content with current date and time
    $logDate = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
    $logContent = @("Log Date: $logDate")
    $markdownContent = @("# Merge Log - $logDate`n")

    # Log duplicates in JSON data
    foreach ($duplicate in $jsonDuplicates.Keys) {
        $logEntry = "Duplicate entry found in JSON data for AAGUID $duplicate with description '$($jsonDuplicates[$duplicate].Description)'"
        $logContent += $logEntry
        $markdownContent += "$logEntry`n"
    }

    # Log duplicates in URL data
    foreach ($duplicate in $urlDuplicates.Keys) {
        $logEntry = "Duplicate entry found in URL data for AAGUID $duplicate with description '$($urlDuplicates[$duplicate].Description)'"
        $logContent += $logEntry
        $markdownContent += "$logEntry`n"
    }

    # Function to check if AAGUID exists in the merged data
    function Test-AAGUIDExists {
        param (
            [string]$aaguid,
            [array]$keys
        )
        foreach ($key in $keys) {
            if ($key.AAGUID -eq $aaguid) {
                return $true
            }
        }
        return $false
    }

    # Validated set of vendors
    $validatedVendors = @(
        "ACS", "Allthenticator", "Arculus", "AuthenTrend", "Atos", "authenton1", "Chunghwa Telecom",
        "Crayonic", "Cryptnox", "Egomet", "Ensurity", "eWBM", "Excelsecu", "Feitian", "FIDO KeyPass", "FT-JCOS",
        "Google", "GoTrust", "HID Global", "Hideez", "Hypersecu", "HYPR", "IDCore", "IDEMIA", "IDmelon", "Thales",
        "ImproveID", "KEY-ID", "KeyXentic", "KONAI", "NEOWAVE", "NXP Semiconductors", "Nymi", "OCTATCO", "OneSpan",
        "OnlyKey", "OpenSK", "Pone Biometrics", "Precision", "RSA", "SafeNet", "Yubico", "Sentry Enterprises",
        "SmartDisplayer", "SoloKeys", "Swissbit", "Taglio", "Token Ring", "TOKEN2", "Identiv", "VALMIDO", "Kensington",
        "VinCSS", "WiSECURE"
    )

    # Function to validate a vendor
    function Test-ValidVendor {
        param (
            [string]$vendor,
            [string]$description
        )
        $attempts = 0
        while ($true) {
            if ($validatedVendors -contains $vendor) {
                return $vendor
            } elseif ($vendor -eq "SKIP") {
                if ($attempts -ge 3) {
                    Write-Warning "Skipping vendor validation for '$description' after 3 attempts"
                    return $vendor
                } else {
                    Write-Warning "Vendor is currently 'SKIP'. Please enter a valid vendor."
                }
            } else {
                Write-Warning "Unknown vendor detected: $vendor"
            }
            $vendor = Read-Host "Enter a valid vendor name for '$description' or type 'SKIP' to bypass validation"
            if ($vendor -eq "") {
                return $vendor
            }
            $attempts++
        }
    }

    $changesMade = $false

   # Loop through the URL data and merge with JSON data
foreach ($urlItem in $urlData) {
    $aaguid = $urlItem.AAGUID
    $description = $urlItem.Description
    if ($jsonDataByAAGUID.ContainsKey($aaguid)) {
        # Update the entry in the new JSON with the URL value
        $jsonItem = $jsonDataByAAGUID[$aaguid]
        foreach ($field in $urlItem.PSObject.Properties.Name) {
            if ($jsonItem.$field -ne $urlItem.$field) {
                $logEntry = "Updated $field for AAGUID $aaguid with description '$description' from '$($jsonItem.$field)' to '$($urlItem.$field)'"
                $logContent += $logEntry
                $markdownContent += "$logEntry`n"
                $jsonItem.$field = $urlItem.$field
                $changesMade = $true
            }
        }
        # Validate the vendor
        $originalVendor = $jsonItem.Vendor
        $jsonItem.Vendor = Test-ValidVendor -vendor $jsonItem.Vendor -description $description
        if ($jsonItem.Vendor -ne $originalVendor) {
            $logEntry = "Updated vendor for AAGUID $($jsonItem.AAGUID) with description '$description' from '$originalVendor' to '$($jsonItem.Vendor)'"
            $logContent += $logEntry
            $markdownContent += "$logEntry`n"
            $changesMade = $true
        }
        if (-not (Test-AAGUIDExists -aaguid $jsonItem.AAGUID -keys $mergedData.keys)) {
            $mergedData.keys += [PSCustomObject]@{
                Vendor = $jsonItem.Vendor
                Description = $jsonItem.Description
                AAGUID = $jsonItem.AAGUID
                Bio = $jsonItem.Bio
                USB = $jsonItem.USB
                NFC = $jsonItem.NFC
                BLE = $jsonItem.BLE
            }
        }
    } else {
        # Prompt for vendor if not available or invalid
        $vendor = Read-Host "Enter vendor for new AAGUID $aaguid with description '$description'"
        $vendor = Test-ValidVendor -vendor $vendor -description $description
        $urlItem | Add-Member -MemberType NoteProperty -Name Vendor -Value $vendor
        if (-not (Test-AAGUIDExists -aaguid $urlItem.AAGUID -keys $mergedData.keys)) {
            $mergedData.keys += [PSCustomObject]@{
                Vendor = $urlItem.Vendor
                Description = $urlItem.Description
                AAGUID = $urlItem.AAGUID
                Bio = $urlItem.Bio
                USB = $urlItem.USB
                NFC = $urlItem.NFC
                BLE = $urlItem.BLE
            }
            $changesMade = $true
        }
        $logEntry = "Added new AAGUID $aaguid with description '$description' and vendor $vendor"
        $logContent += $logEntry
        $markdownContent += "$logEntry`n"
    }
}

# Check for AAGUIDs in JSON data but not in URL data and remove them
foreach ($jsonItem in $jsonData.keys) {
    if (-not $urlDataByAAGUID.ContainsKey($jsonItem.AAGUID)) {
        $logEntry = "Removed AAGUID $($jsonItem.AAGUID) with description '$($jsonItem.Description)' from JSON data"
        $logContent += $logEntry
        $markdownContent += "$logEntry`n"
        $changesMade = $true
    } else {
        $originalVendor = $jsonItem.Vendor
        $jsonItem.Vendor = Test-ValidVendor -vendor $jsonItem.Vendor -description $jsonItem.Description
        if ($jsonItem.Vendor -ne $originalVendor) {
            $logEntry = "Updated vendor for AAGUID $($jsonItem.AAGUID) with description '$($jsonItem.Description)' from '$originalVendor' to '$($jsonItem.Vendor)'"
            $logContent += $logEntry
            $markdownContent += "$logEntry`n"
            $changesMade = $true
        }
        if (-not (Test-AAGUIDExists -aaguid $jsonItem.AAGUID -keys $mergedData.keys)) {
            $mergedData.keys += [PSCustomObject]@{
                Vendor = $jsonItem.Vendor
                Description = $jsonItem.Description
                AAGUID = $jsonItem.AAGUID
                Bio = $jsonItem.Bio
                USB = $jsonItem.USB
                NFC = $jsonItem.NFC
                BLE = $jsonItem.BLE
            }
        }
    }
}

# Update the databaseLastUpdated field if changes were made
if ($changesMade) {
    $mergedData.metadata.databaseLastUpdated = (Get-Date).ToString("yyyy-MM-ddTHH:mm:ssZ")
}

# Save the new JSON structure back to the original JSON file
$mergedData | ConvertTo-Json -Depth 10 | Set-Content -Path $JsonFilePath

# Compare current markdown content with the last one
$lastMarkdownContent = if (Test-Path -Path $MarkdownFilePath) { Get-Content -Raw -Path $MarkdownFilePath } else { "" }
$currentMarkdownContent = $markdownContent -join "`n"

# Save the markdown content to the markdown file if there are changes and it's different from the last one
if ($changesMade -and $currentMarkdownContent -ne $lastMarkdownContent) {
    if (Test-Path -Path $MarkdownFilePath) {
        $markdownContent | Out-File -FilePath $MarkdownFilePath -Append
    } else {
        $markdownContent | Out-File -FilePath $MarkdownFilePath
    }
    Write-Host "Markdown log saved to $MarkdownFilePath"
} else {
    $logContent += "No changes"
    Write-Host "No changes detected, Markdown log not updated"
}

# Always save the log content to the log file
$logContent | Out-File -FilePath $LogFilePath -Append
Write-Host "Log file saved to $LogFilePath"

Write-Host "Merged data saved to $JsonFilePath"
}