Scripts/Sync-FidoKeysWithDocs.ps1
|
<# .SYNOPSIS Synchronizes local FidoKeys.json with the latest Microsoft Entra FIDO vendor table. .DESCRIPTION Fetches the live vendor table from Microsoft Entra docs, compares against local FidoKeys.json, displays differences in terminal, and prompts user to approve/reject changes. Generates markdown report of all changes and optionally updates FidoKeys.json with approved modifications. .PARAMETER Owner GitHub repository owner (default: MicrosoftDocs) .PARAMETER Repo GitHub repository name (default: entra-docs) .PARAMETER Ref Branch or commit SHA to fetch from (default: main) .PARAMETER BaseRef Older branch or commit SHA for doc-to-doc comparison (e.g., compare Dec 8 to main). When provided, compares old doc vs new doc instead of new doc vs local FidoKeys.json .PARAMETER FilePath Path to vendor table in repo (default: docs/identity/authentication/concept-fido2-hardware-vendor.md) .PARAMETER LocalPath Path to local FidoKeys.json (default: Assets/FidoKeys.json) .PARAMETER AutoApprove Automatically approve all changes without prompting .PARAMETER PreviewOnly Show changes without prompting or updating .PARAMETER OutputMarkdown Custom output markdown path (default: auto-timestamped) .EXAMPLE ./Sync-FidoKeysWithDocs.ps1 Shows differences interactively, prompts for each change .EXAMPLE ./Sync-FidoKeysWithDocs.ps1 -PreviewOnly Shows differences only; no prompts and no updates #> [CmdletBinding()] param( [string]$Owner = 'MicrosoftDocs', [string]$Repo = 'entra-docs', [string]$Ref = 'main', [string]$BaseRef = '', [string]$FilePath = 'docs/identity/authentication/concept-fido2-hardware-vendor.md', [string]$LocalPath = 'Assets/FidoKeys.json', [switch]$AutoApprove, [switch]$PreviewOnly, [string]$OutputMarkdown = "FIDO_SYNC_$(Get-Date -Format 'yyyyMMdd_HHmmss').md" ) function Get-DocFile { param( [string]$Owner, [string]$Repo, [string]$Ref, [string]$Path ) $url = "https://raw.githubusercontent.com/$Owner/$Repo/$Ref/$Path" try { $resp = Invoke-WebRequest -Uri $url -UseBasicParsing -ErrorAction Stop return $resp.Content } catch { throw "Failed to fetch doc file from ${url}: $_" } } function ConvertFrom-DocTable { param([string]$content) $entries = @() $lines = $content -split "`r`n|`n|`r" $headerSeen = $false $lineIndex = 0 $emitLimit = 5 foreach ($line in $lines) { $lineIndex++ if ([string]::IsNullOrWhiteSpace($line)) { continue } $cleanLine = ($line -replace '^\|\s*', '') -replace '\s*\|$', '' if (-not $headerSeen -and $cleanLine -match '^\s*Description\s*\|\s*AAGUID\s*\|\s*Bio\s*\|\s*USB\s*\|\s*NFC\s*\|\s*BLE\s*$') { $headerSeen = $true Write-Verbose "Header detected at line $lineIndex" continue } if ($headerSeen -and ($cleanLine -match '^##' -or $cleanLine -match '^Related content' -or $cleanLine -match '^For more')) { Write-Verbose "End of table detected at line $lineIndex" break } if ($headerSeen -and ($cleanLine -split '\|').Count -ge 6) { $parts = $cleanLine -split '\|' | ForEach-Object { $_.Trim() } if ($parts[0] -match '^-+$') { continue } $aaguid = $parts[1] if ($aaguid -notmatch '^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$') { continue } $mapToken = { param([string]$t) if ($t -match '✅|✅|✔|✔|✓|yes|true') { return '✅' } if ($t -match '❌|❌|✗|no|false') { return '❌' } return '❌' } $entry = @{ Description = $parts[0] AAGUID = $aaguid.ToLower() Bio = & $mapToken $parts[2] USB = & $mapToken $parts[3] NFC = & $mapToken $parts[4] BLE = & $mapToken $parts[5] } if ($entries.Count -lt $emitLimit) { Write-Verbose ("Parsed entry #{0}: {1} [{2}] Bio={3} USB={4} NFC={5} BLE={6}" -f ($entries.Count+1), $entry.Description, $entry.AAGUID, $entry.Bio, $entry.USB, $entry.NFC, $entry.BLE) } $entries += $entry } } Write-Verbose "Parsed $($entries.Count) entries from doc content" if ($entries.Count -eq 0) { Write-Verbose "No rows parsed; header may not have been detected" } return $entries } function Get-LocalDatabase { <# Get and parse local FidoKeys.json #> param([string]$Path) if (-not (Test-Path $Path)) { throw "Local FidoKeys.json not found at: $Path" } try { $json = Get-Content -Path $Path -Raw | ConvertFrom-Json return $json.keys } catch { throw "Failed to parse FidoKeys.json: $_" } } function Compare-Databases { <# Compare doc entries against local data by AAGUID #> param($docEntries, $localEntries) $localMap = @{} foreach ($entry in $localEntries) { if ($entry.AAGUID) { $localMap[$entry.AAGUID.ToLower()] = $entry } } $added = @() $modified = @() $removed = @() # Find added and modified foreach ($doc in $docEntries) { if ($localMap.ContainsKey($doc.AAGUID)) { $local = $localMap[$doc.AAGUID] if ($doc.Bio -ne $local.Bio -or $doc.USB -ne $local.USB -or $doc.NFC -ne $local.NFC -or $doc.BLE -ne $local.BLE) { $modified += @{ AAGUID = $doc.AAGUID Description = $doc.Description OldData = $local NewData = $doc } } } else { $added += $doc } } # Find removed $docAaguidSet = @{} foreach ($doc in $docEntries) { $docAaguidSet[$doc.AAGUID] = $true } foreach ($local in $localEntries) { if ($local.AAGUID -and -not $docAaguidSet.ContainsKey($local.AAGUID.ToLower())) { $removed += $local } } return @{ Added = $added Modified = $modified Removed = $removed } } function Show-Changes { <# Display changes in terminal with colors #> param($comparison) Write-Host "`n" + ("=" * 80) -ForegroundColor Cyan Write-Host "FIDO KEYS SYNC SUMMARY" -ForegroundColor Cyan Write-Host ("=" * 80) -ForegroundColor Cyan Write-Host "`n✅ ADDED ($($comparison.Added.Count)):" -ForegroundColor Green if ($comparison.Added.Count -eq 0) { Write-Host " (none)" -ForegroundColor DarkGray } else { foreach ($item in $comparison.Added) { Write-Host " • $($item.Description) [$($item.AAGUID)]" -ForegroundColor Green Write-Host " Bio: $($item.Bio) USB: $($item.USB) NFC: $($item.NFC) BLE: $($item.BLE)" -ForegroundColor DarkGreen } } Write-Host "`n⚠️ MODIFIED ($($comparison.Modified.Count)):" -ForegroundColor Yellow if ($comparison.Modified.Count -eq 0) { Write-Host " (none)" -ForegroundColor DarkGray } else { foreach ($item in $comparison.Modified) { Write-Host " • $($item.Description) [$($item.AAGUID)]" -ForegroundColor Yellow $changes = @() if ($item.OldData.Bio -ne $item.NewData.Bio) { $changes += "Bio: $($item.OldData.Bio) → $($item.NewData.Bio)" } if ($item.OldData.USB -ne $item.NewData.USB) { $changes += "USB: $($item.OldData.USB) → $($item.NewData.USB)" } if ($item.OldData.NFC -ne $item.NewData.NFC) { $changes += "NFC: $($item.OldData.NFC) → $($item.NewData.NFC)" } if ($item.OldData.BLE -ne $item.NewData.BLE) { $changes += "BLE: $($item.OldData.BLE) → $($item.NewData.BLE)" } foreach ($change in $changes) { Write-Host " $change" -ForegroundColor DarkYellow } } } Write-Host "`n❌ REMOVED ($($comparison.Removed.Count)):" -ForegroundColor Red if ($comparison.Removed.Count -eq 0) { Write-Host " (none)" -ForegroundColor DarkGray } else { foreach ($item in $comparison.Removed) { Write-Host " • $($item.Description) [$($item.AAGUID)]" -ForegroundColor Red Write-Host " Bio: $($item.Bio) USB: $($item.USB) NFC: $($item.NFC) BLE: $($item.BLE)" -ForegroundColor DarkRed } } Write-Host "`n" + ("=" * 80) -ForegroundColor Cyan } function Get-UserApproval { <# Interactive prompt for each change category #> param($comparison) $approvedChanges = @{ AddedApproved = [System.Collections.ArrayList]@() ModifiedApproved = [System.Collections.ArrayList]@() RemovedApproved = [System.Collections.ArrayList]@() AddedRejected = [System.Collections.ArrayList]@() ModifiedRejected = [System.Collections.ArrayList]@() RemovedRejected = [System.Collections.ArrayList]@() } # Added if ($comparison.Added.Count -gt 0) { Write-Host "`n📌 ADDING $($comparison.Added.Count) NEW KEY(S):" -ForegroundColor Green foreach ($item in $comparison.Added) { $prompt = Read-Host " Add '$($item.Description)'? (y/n/skip)" if ($prompt -eq 'y') { [void]$approvedChanges.AddedApproved.Add($item) } else { [void]$approvedChanges.AddedRejected.Add($item) } } } # Modified if ($comparison.Modified.Count -gt 0) { Write-Host "`n🔄 MODIFYING $($comparison.Modified.Count) KEY(S):" -ForegroundColor Yellow foreach ($item in $comparison.Modified) { Write-Host " '$($item.Description)' [$($item.AAGUID)]" -ForegroundColor Yellow $changes = @() if ($item.OldData.Bio -ne $item.NewData.Bio) { $changes += "Bio: $($item.OldData.Bio) → $($item.NewData.Bio)" } if ($item.OldData.USB -ne $item.NewData.USB) { $changes += "USB: $($item.OldData.USB) → $($item.NewData.USB)" } if ($item.OldData.NFC -ne $item.NewData.NFC) { $changes += "NFC: $($item.OldData.NFC) → $($item.NewData.NFC)" } if ($item.OldData.BLE -ne $item.NewData.BLE) { $changes += "BLE: $($item.OldData.BLE) → $($item.NewData.BLE)" } foreach ($change in $changes) { Write-Host " $change" -ForegroundColor DarkYellow } $prompt = Read-Host " Apply changes? (y/n/skip)" if ($prompt -eq 'y') { [void]$approvedChanges.ModifiedApproved.Add($item) } else { [void]$approvedChanges.ModifiedRejected.Add($item) } } } # Removed if ($comparison.Removed.Count -gt 0) { Write-Host "`n🗑️ REMOVING $($comparison.Removed.Count) KEY(S):" -ForegroundColor Red foreach ($item in $comparison.Removed) { $prompt = Read-Host " Remove '$($item.Description)'? (y/n/skip)" if ($prompt -eq 'y') { [void]$approvedChanges.RemovedApproved.Add($item) } else { [void]$approvedChanges.RemovedRejected.Add($item) } } } return $approvedChanges } function Write-SyncMarkdown { <# Generate markdown report #> param($comparison, $approvedChanges, [string]$output) $sb = [System.Text.StringBuilder]::new() [void]$sb.AppendLine("# FIDO Keys Sync Report") [void]$sb.AppendLine() [void]$sb.AppendLine("Generated: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')") [void]$sb.AppendLine() # Summary [void]$sb.AppendLine("## Summary") [void]$sb.AppendLine() [void]$sb.AppendLine("| Category | Found | Approved | Rejected |") [void]$sb.AppendLine("|----------|-------|----------|----------|") [void]$sb.AppendLine("| Added | $($comparison.Added.Count) | $($approvedChanges.AddedApproved.Count) | $($approvedChanges.AddedRejected.Count) |") [void]$sb.AppendLine("| Modified | $($comparison.Modified.Count) | $($approvedChanges.ModifiedApproved.Count) | $($approvedChanges.ModifiedRejected.Count) |") [void]$sb.AppendLine("| Removed | $($comparison.Removed.Count) | $($approvedChanges.RemovedApproved.Count) | $($approvedChanges.RemovedRejected.Count) |") [void]$sb.AppendLine() # Added [void]$sb.AppendLine("## ✅ Added Keys ($($comparison.Added.Count))") [void]$sb.AppendLine() if ($comparison.Added.Count -eq 0) { [void]$sb.AppendLine("No keys were added.") } else { [void]$sb.AppendLine("| Status | Description | AAGUID | Bio | USB | NFC | BLE |") [void]$sb.AppendLine("|--------|-------------|--------|-----|-----|-----|-----|") foreach ($item in $comparison.Added) { $status = if ($approvedChanges.AddedApproved -contains $item) { "✅" } else { "❌" } [void]$sb.AppendLine("| $status | $($item.Description) | $($item.AAGUID) | $($item.Bio) | $($item.USB) | $($item.NFC) | $($item.BLE) |") } } [void]$sb.AppendLine() # Modified [void]$sb.AppendLine("## ⚠️ Modified Keys ($($comparison.Modified.Count))") [void]$sb.AppendLine() if ($comparison.Modified.Count -eq 0) { [void]$sb.AppendLine("No keys were modified.") } else { foreach ($item in ($comparison.Modified | Sort-Object AAGUID)) { $status = if ($approvedChanges.ModifiedApproved -contains $item) { "✅" } else { "❌" } [void]$sb.AppendLine("### $status $($item.Description)") [void]$sb.AppendLine("- **AAGUID:** $($item.AAGUID)") if ($item.OldData.Bio -ne $item.NewData.Bio) { [void]$sb.AppendLine("- **Bio:** $($item.OldData.Bio) → $($item.NewData.Bio)") } if ($item.OldData.USB -ne $item.NewData.USB) { [void]$sb.AppendLine("- **USB:** $($item.OldData.USB) → $($item.NewData.USB)") } if ($item.OldData.NFC -ne $item.NewData.NFC) { [void]$sb.AppendLine("- **NFC:** $($item.OldData.NFC) → $($item.NewData.NFC)") } if ($item.OldData.BLE -ne $item.NewData.BLE) { [void]$sb.AppendLine("- **BLE:** $($item.OldData.BLE) → $($item.NewData.BLE)") } [void]$sb.AppendLine() } } # Removed [void]$sb.AppendLine("## ❌ Removed Keys ($($comparison.Removed.Count))") [void]$sb.AppendLine() if ($comparison.Removed.Count -eq 0) { [void]$sb.AppendLine("No keys were removed.") } else { [void]$sb.AppendLine("| Status | Description | AAGUID | Bio | USB | NFC | BLE |") [void]$sb.AppendLine("|--------|-------------|--------|-----|-----|-----|-----|") foreach ($item in $comparison.Removed) { $status = if ($approvedChanges.RemovedApproved -contains $item) { "✅" } else { "❌" } [void]$sb.AppendLine("| $status | $($item.Description) | $($item.AAGUID) | $($item.Bio) | $($item.USB) | $($item.NFC) | $($item.BLE) |") } } [void]$sb.AppendLine() Set-Content -Path $output -Value $sb.ToString() -Encoding UTF8 } function Write-PublicDocComparisonMarkdown { <# Generate a professional public-facing markdown report for doc-to-doc comparisons #> param($comparison, [string]$baseRef, [string]$newRef, [string]$output) $sb = [System.Text.StringBuilder]::new() # Professional header [void]$sb.AppendLine("# FIDO2 Hardware Vendor Table Update Report") [void]$sb.AppendLine() [void]$sb.AppendLine("**Report Generated:** $(Get-Date -Format 'MMMM d, yyyy HH:mm:ss UTC')") [void]$sb.AppendLine() [void]$sb.AppendLine("## Comparison Details") [void]$sb.AppendLine() [void]$sb.AppendLine("| Metric | Value |") [void]$sb.AppendLine("|--------|-------|") [void]$sb.AppendLine("| Base Version | $baseRef |") [void]$sb.AppendLine("| New Version | $newRef |") [void]$sb.AppendLine("| New Entries | $($comparison.Added.Count) |") [void]$sb.AppendLine("| Updated Entries | $($comparison.Modified.Count) |") [void]$sb.AppendLine("| Removed Entries | $($comparison.Removed.Count) |") [void]$sb.AppendLine() # Executive Summary [void]$sb.AppendLine("## Summary") [void]$sb.AppendLine() if ($comparison.Added.Count -eq 0 -and $comparison.Modified.Count -eq 0 -and $comparison.Removed.Count -eq 0) { [void]$sb.AppendLine("No changes detected between versions.") } else { [void]$sb.AppendLine("The Microsoft Entra FIDO2 hardware vendor table has been updated with the following changes:") [void]$sb.AppendLine() if ($comparison.Added.Count -gt 0) { [void]$sb.AppendLine("- **$($comparison.Added.Count) new authenticators** have been added to the supported vendors list") } if ($comparison.Modified.Count -gt 0) { [void]$sb.AppendLine("- **$($comparison.Modified.Count) authenticators** have been updated with new capability information") } if ($comparison.Removed.Count -gt 0) { [void]$sb.AppendLine("- **$($comparison.Removed.Count) authenticators** have been removed from the supported vendors list") } } [void]$sb.AppendLine() # Added entries if ($comparison.Added.Count -gt 0) { [void]$sb.AppendLine("## ✅ New Authenticators ($($comparison.Added.Count))") [void]$sb.AppendLine() [void]$sb.AppendLine("The following authenticators are now supported:") [void]$sb.AppendLine() foreach ($item in ($comparison.Added | Sort-Object Description)) { [void]$sb.AppendLine("### $($item.Description)") [void]$sb.AppendLine() [void]$sb.AppendLine("**AAGUID:** ``$($item.AAGUID)``") [void]$sb.AppendLine() [void]$sb.AppendLine("**Supported Interfaces:**") [void]$sb.AppendLine() [void]$sb.AppendLine("| Interface | Supported |") [void]$sb.AppendLine("|-----------|-----------|") [void]$sb.AppendLine("| Biometric | $($item.Bio) |") [void]$sb.AppendLine("| USB | $($item.USB) |") [void]$sb.AppendLine("| NFC | $($item.NFC) |") [void]$sb.AppendLine("| BLE | $($item.BLE) |") [void]$sb.AppendLine() } } # Modified entries if ($comparison.Modified.Count -gt 0) { [void]$sb.AppendLine("## ⚠️ Updated Authenticators ($($comparison.Modified.Count))") [void]$sb.AppendLine() [void]$sb.AppendLine("The following authenticators have been updated with new capability information:") [void]$sb.AppendLine() foreach ($item in ($comparison.Modified | Sort-Object Description)) { [void]$sb.AppendLine("### $($item.Description)") [void]$sb.AppendLine() [void]$sb.AppendLine("**AAGUID:** ``$($item.AAGUID)``") [void]$sb.AppendLine() [void]$sb.AppendLine("**Changes:**") [void]$sb.AppendLine() $hasChanges = $false if ($item.OldData.Bio -ne $item.NewData.Bio) { [void]$sb.AppendLine("- Biometric: $($item.OldData.Bio) → $($item.NewData.Bio)") $hasChanges = $true } if ($item.OldData.USB -ne $item.NewData.USB) { [void]$sb.AppendLine("- USB: $($item.OldData.USB) → $($item.NewData.USB)") $hasChanges = $true } if ($item.OldData.NFC -ne $item.NewData.NFC) { [void]$sb.AppendLine("- NFC: $($item.OldData.NFC) → $($item.NewData.NFC)") $hasChanges = $true } if ($item.OldData.BLE -ne $item.NewData.BLE) { [void]$sb.AppendLine("- BLE: $($item.OldData.BLE) → $($item.NewData.BLE)") $hasChanges = $true } if (-not $hasChanges) { [void]$sb.AppendLine("No capability changes recorded.") } [void]$sb.AppendLine() } } # Removed entries (if any) if ($comparison.Removed.Count -gt 0) { [void]$sb.AppendLine("## ❌ Removed Authenticators ($($comparison.Removed.Count))") [void]$sb.AppendLine() [void]$sb.AppendLine("The following authenticators are no longer in the supported vendors list:") [void]$sb.AppendLine() foreach ($item in ($comparison.Removed | Sort-Object Description)) { [void]$sb.AppendLine("- **$($item.Description)** (``$($item.AAGUID)``)") } [void]$sb.AppendLine() } # Footer [void]$sb.AppendLine("---") [void]$sb.AppendLine() [void]$sb.AppendLine("*This report was automatically generated by comparing Microsoft Entra FIDO2 vendor table versions.*") Set-Content -Path $output -Value $sb.ToString() -Encoding UTF8 } function Update-LocalDatabase { <# Apply approved changes to FidoKeys.json #> param($localData, $approvedChanges, [string]$path) # Backup original $backup = "$path.backup.$(Get-Date -Format 'yyyyMMdd_HHmmss')" Copy-Item -Path $path -Destination $backup Write-Host "Backup created: $backup" -ForegroundColor Cyan # Load full JSON to preserve metadata $json = Get-Content -Path $path -Raw | ConvertFrom-Json # Build AAGUID map of local entries with indices $localMap = @{} $keysToDelete = [System.Collections.ArrayList]::new() for ($i = 0; $i -lt $json.keys.Count; $i++) { if ($json.keys[$i].AAGUID) { $localMap[$json.keys[$i].AAGUID.ToLower()] = $i } } # Mark removals (delete from highest index to lowest to preserve indices) foreach ($item in $approvedChanges.RemovedApproved) { $aaguidLower = $item.AAGUID.ToLower() if ($localMap.ContainsKey($aaguidLower)) { $idx = $localMap[$aaguidLower] [void]$keysToDelete.Add($idx) } } # Sort indices in descending order and remove $indicesToDelete = @($keysToDelete | Sort-Object -Descending) foreach ($idx in $indicesToDelete) { if ($idx -ge 0 -and $idx -lt $json.keys.Count) { $json.keys = @($json.keys[0..($idx-1)] + $json.keys[($idx+1)..($json.keys.Count-1)]) } } # Rebuild map after deletions $localMap.Clear() for ($i = 0; $i -lt $json.keys.Count; $i++) { if ($json.keys[$i].AAGUID) { $localMap[$json.keys[$i].AAGUID.ToLower()] = $i } } # Apply modifications foreach ($item in $approvedChanges.ModifiedApproved) { $idx = $localMap[$item.AAGUID.ToLower()] if ($null -ne $idx) { $json.keys[$idx].Bio = $item.NewData.Bio $json.keys[$idx].USB = $item.NewData.USB $json.keys[$idx].NFC = $item.NewData.NFC $json.keys[$idx].BLE = $item.NewData.BLE } } # Apply additions foreach ($item in $approvedChanges.AddedApproved) { $newEntry = @{ Vendor = "Unknown" Description = $item.Description AAGUID = $item.AAGUID Bio = $item.Bio USB = $item.USB NFC = $item.NFC BLE = $item.BLE Version = "Unknown" ValidVendor = "Unknown" metadataStatement = @{} statusReports = @() timeOfLastStatusChange = (Get-Date -Format 'yyyy-MM-dd') } $json.keys += @($newEntry) } # Update metadata $json.metadata.databaseLastUpdated = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' # Write updated JSON $json | ConvertTo-Json -Depth 10 | Set-Content -Path $path -Encoding UTF8 Write-Host "FidoKeys.json updated successfully" -ForegroundColor Green } # ===== Main Execution ===== try { # Determine comparison mode $isDocToDocMode = -not [string]::IsNullOrWhiteSpace($BaseRef) if ($isDocToDocMode) { Write-Host "Comparing docs: $BaseRef vs $Ref" -ForegroundColor Cyan Write-Host "Fetching older doc from $BaseRef..." -ForegroundColor Cyan $baseDocContent = Get-DocFile -Owner $Owner -Repo $Repo -Ref $BaseRef -Path $FilePath Write-Host "Parsing older doc..." -ForegroundColor Cyan $baseDocEntries = ConvertFrom-DocTable -content $baseDocContent Write-Host "Found $($baseDocEntries.Count) entries in base doc" -ForegroundColor Green Write-Host "Fetching newer doc from $Ref..." -ForegroundColor Cyan $newDocContent = Get-DocFile -Owner $Owner -Repo $Repo -Ref $Ref -Path $FilePath Write-Host "Parsing newer doc..." -ForegroundColor Cyan $newDocEntries = ConvertFrom-DocTable -content $newDocContent Write-Host "Found $($newDocEntries.Count) entries in new doc" -ForegroundColor Green Write-Host "Comparing docs..." -ForegroundColor Cyan $comparison = Compare-Databases -docEntries $newDocEntries -localEntries $baseDocEntries } else { Write-Host "Fetching vendor table from Microsoft Entra docs (ref: $Ref)..." -ForegroundColor Cyan $docContent = Get-DocFile -Owner $Owner -Repo $Repo -Ref $Ref -Path $FilePath Write-Host "Parsing vendor table..." -ForegroundColor Cyan $docEntries = ConvertFrom-DocTable -content $docContent Write-Host "Found $($docEntries.Count) entries in docs" -ForegroundColor Green if ($docEntries.Count -eq 0) { Write-Host "WARNING: No entries found in docs. This may indicate a parsing issue." -ForegroundColor Yellow Write-Host "First 500 chars of content:" -ForegroundColor Yellow Write-Host ($docContent.Substring(0, [Math]::Min(500, $docContent.Length))) -ForegroundColor Yellow } Write-Host "Loading local FidoKeys.json..." -ForegroundColor Cyan $localEntries = Get-LocalDatabase -Path $LocalPath Write-Host "Found $($localEntries.Count) entries locally" -ForegroundColor Green Write-Host "Comparing databases..." -ForegroundColor Cyan $comparison = Compare-Databases -docEntries $docEntries -localEntries $localEntries } # Show changes Show-Changes -comparison $comparison # Process changes if ($isDocToDocMode -or $PreviewOnly) { Write-Host "`n⏸️ Preview mode: No changes will be applied." -ForegroundColor Yellow $approvedChanges = @{ AddedApproved = [System.Collections.ArrayList]@() ModifiedApproved = [System.Collections.ArrayList]@() RemovedApproved = [System.Collections.ArrayList]@() AddedRejected = $comparison.Added ModifiedRejected = $comparison.Modified RemovedRejected = $comparison.Removed } } elseif ($AutoApprove) { Write-Host "`n✅ Auto-approve mode: Accepting all changes." -ForegroundColor Green $approvedChanges = @{ AddedApproved = [System.Collections.ArrayList]@($comparison.Added) ModifiedApproved = [System.Collections.ArrayList]@($comparison.Modified) RemovedApproved = [System.Collections.ArrayList]@($comparison.Removed) AddedRejected = [System.Collections.ArrayList]@() ModifiedRejected = [System.Collections.ArrayList]@() RemovedRejected = [System.Collections.ArrayList]@() } } else { Write-Host "`n🔍 Interactive mode: Prompting for each change." -ForegroundColor Yellow $approvedChanges = Get-UserApproval -comparison $comparison } # Write markdown report Write-Host "`nGenerating sync report..." -ForegroundColor Cyan if ($isDocToDocMode) { Write-PublicDocComparisonMarkdown -comparison $comparison -baseRef $BaseRef -newRef $Ref -output $OutputMarkdown } else { Write-SyncMarkdown -comparison $comparison -approvedChanges $approvedChanges -output $OutputMarkdown } Write-Host "Report written to: $OutputMarkdown" -ForegroundColor Green # Update local database if changes approved $totalApproved = $approvedChanges.AddedApproved.Count + $approvedChanges.ModifiedApproved.Count + $approvedChanges.RemovedApproved.Count if ($totalApproved -gt 0 -and -not $PreviewOnly) { Write-Host "`nApplying $totalApproved approved changes..." -ForegroundColor Cyan Update-LocalDatabase -localData $localEntries -approvedChanges $approvedChanges -path $LocalPath } else { Write-Host "`nNo changes to apply." -ForegroundColor Yellow } Write-Host "`n✨ Sync complete!" -ForegroundColor Green } catch { Write-Host "Error: $_" -ForegroundColor Red exit 1 } |