Functions/Private/Merge-FidoData.ps1
<# .SYNOPSIS Merges FIDO data from a JSON file with data from a specified URL and logs the changes. .DESCRIPTION The `Merge-FidoData` function reads FIDO data from a JSON file and merges it with data retrieved from a specified URL. It logs any changes made during the merge process to both a text log file and a markdown log file. The function also validates vendor names against a predefined list and prompts the user for valid vendor names if necessary. .PARAMETER Url The URL from which to retrieve the FIDO data. 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 data. If not provided, a default path is constructed. .PARAMETER LogFilePath The file path to the text log file where changes will be logged. Defaults to "merge_log.txt". .PARAMETER MarkdownFilePath The file path to the markdown log file where changes will be logged in markdown format. Defaults to "merge_log.md". .EXAMPLE PS> Merge-FidoData -JsonFilePath "C:\Path\To\FidoKeys.json" -LogFilePath "C:\Path\To\merge_log.txt" -MarkdownFilePath "C:\Path\To\merge_log.md" Merges FIDO data from the specified JSON file with data from the default URL and logs the changes to the specified log files. .EXAMPLE PS> Merge-FidoData -Url "https://example.com/fido-data" -JsonFilePath "C:\Path\To\FidoKeys.json" Merges FIDO data from the specified JSON file with data from the specified URL and logs the changes to the default log files. .NOTES - The function validates vendor names against a predefined list of vendors. - If a vendor name is not valid, the user is prompted to enter a valid vendor name or to skip validation. - The function logs duplicate entries found in both the JSON and URL data. - The function updates the JSON file with the merged data and logs the changes to the specified log 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 = "merge_log.txt", [Parameter()] [string]$MarkdownFilePath = "merge_log.md" ) # 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" } # 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 } } } } # 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" if ($changesMade) { $mergedData.metadata.databaseLastUpdated = (Get-Date).ToString("yyyy-MM-ddTHH:mm:ssZ") } # 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" } |