├── .gitignore ├── Assets ├── Webss1.png └── valid_vendors.json ├── .vscode ├── settings.json ├── db-keys.schema.json └── fido2-metadata.schema.json ├── EntraFIDOFinder.psm1 ├── Scripts ├── Test-GHAAGUIDExists.ps1 ├── Export-GHEntraFido.ps1 ├── Test-GHValidVendor.ps1 ├── Merge-GHFIDOMetaData.ps1 └── Merge-GHFidoData.ps1 ├── .github └── workflows │ ├── Merge-FIDO-Metadata.yml │ ├── CopyFIDOKey.yml │ ├── GHMerge.yml │ └── GHMergeDev.yml ├── Functions ├── Private │ ├── Export-EntraFido.ps1 │ └── Merge-FidoData.ps1 └── Public │ ├── Get-FIDODbLog.ps1 │ ├── Show-FIDODbVersion.ps1 │ └── Find-FIDOKey.ps1 ├── EntraFIDOFinder.psd1 ├── README.md ├── CHANGELOG.md └── Explorer └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | merge_log.txt 2 | Scripts/Convert-FIDOKeyToStandardFormat.ps1 3 | -------------------------------------------------------------------------------- /Assets/Webss1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevClate/EntraFIDOFinder/HEAD/Assets/Webss1.png -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "json.schemas": [ 3 | { 4 | "fileMatch": [ 5 | "db-keys.json" 6 | ], 7 | "url": "./.vscode/db-keys.schema.json" 8 | }, 9 | { 10 | "fileMatch": [ 11 | "FidoKeys.json" 12 | ], 13 | "url": "./.vscode/fido2-metadata.schema.json" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /EntraFIDOFinder.psm1: -------------------------------------------------------------------------------- 1 | $FunctionFiles = $("$PSScriptRoot\Functions\Public\","$PSScriptRoot\Functions\Private\") | Get-ChildItem -File -Recurse -Include "*.ps1" -ErrorAction SilentlyContinue 2 | 3 | foreach ($FunctionFile in $FunctionFiles) { 4 | try { 5 | . $FunctionFile.FullName 6 | } catch { 7 | Write-Error -Message "Failed to import function: '$($FunctionFile.FullName)': $_" 8 | } 9 | } 10 | 11 | # Export other functions 12 | Export-ModuleMember -Function $FunctionFiles.BaseName -------------------------------------------------------------------------------- /.vscode/db-keys.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "Database-of-Keys", 4 | "type": "object", 5 | "properties": { 6 | "metadata": { 7 | "type": "object", 8 | "properties": { 9 | "databaseLastUpdated": { "type": "string", "format": "date-time" }, 10 | "databaseLastChecked": { "type": "string", "format": "date-time" } 11 | }, 12 | "required": [ "databaseLastUpdated", "databaseLastChecked" ], 13 | "additionalProperties": false 14 | }, 15 | "keys": { 16 | "type": "array", 17 | "items": { 18 | "$ref": "./fido2-metadata.schema.json" 19 | } 20 | } 21 | }, 22 | "required": [ "metadata", "keys" ], 23 | "additionalProperties": false 24 | } -------------------------------------------------------------------------------- /Scripts/Test-GHAAGUIDExists.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Tests if a given AAGUID exists in a list of keys. 4 | 5 | .DESCRIPTION 6 | This function checks if a specified AAGUID is present in an array of keys. 7 | It iterates through each key and compares its AAGUID property with the provided AAGUID. 8 | 9 | .PARAMETER aaguid 10 | The AAGUID to search for in the keys array. 11 | 12 | .PARAMETER keys 13 | An array of keys, each containing an AAGUID property. 14 | 15 | .RETURNS 16 | [bool] $true if the AAGUID is found in the keys array, otherwise $false. 17 | 18 | .EXAMPLE 19 | $keys = @( 20 | @{ AAGUID = "1234" }, 21 | @{ AAGUID = "5678" } 22 | ) 23 | Test-GHAAGUIDExists -aaguid "1234" -keys $keys 24 | # Returns: $true 25 | 26 | .EXAMPLE 27 | $keys = @( 28 | @{ AAGUID = "1234" }, 29 | @{ AAGUID = "5678" } 30 | ) 31 | Test-GHAAGUIDExists -aaguid "9999" -keys $keys 32 | # Returns: $false 33 | #> 34 | function Test-GHAAGUIDExists { 35 | param ( 36 | [string]$aaguid, 37 | [array]$keys 38 | ) 39 | foreach ($key in $keys) { 40 | if ($key.AAGUID -eq $aaguid) { 41 | return $true 42 | } 43 | } 44 | return $false 45 | } -------------------------------------------------------------------------------- /.github/workflows/Merge-FIDO-Metadata.yml: -------------------------------------------------------------------------------- 1 | name: Main - Merge FIDO Metadata 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: '0 4 1 * *' 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | merge-fido-metadata: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout repository 17 | uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 0 20 | ref: main 21 | 22 | - name: Install JWTDetails Module 23 | shell: pwsh 24 | run: Install-Module -Name JWTDetails -Force -Scope CurrentUser 25 | 26 | - name: Run Merge-FidoMetaData.ps1 27 | shell: pwsh 28 | run: | 29 | Import-Module -Name JWTDetails 30 | ./Scripts/Merge-GHFIDOMetaData.ps1 31 | 32 | - name: Configure Git 33 | run: | 34 | git config --global user.name 'DevClate' 35 | git config --global user.email 'clate@clatent.com' 36 | 37 | - name: Commit changes 38 | run: | 39 | git add Assets/FidoKeys.json FAmerge_log.txt 40 | git commit -m "Update FidoKeys.json and FAmerge_log.txt" || echo "No changes to commit" 41 | 42 | - name: Push changes 43 | uses: ad-m/github-push-action@v0.6.0 44 | with: 45 | github_token: ${{ secrets.GITHUB_TOKEN }} 46 | branch: main 47 | -------------------------------------------------------------------------------- /.github/workflows/CopyFIDOKey.yml: -------------------------------------------------------------------------------- 1 | name: Main - Update FidoKeys in gh-pages 2 | 3 | on: 4 | workflow_run: 5 | workflows: ["Main - Merge"] 6 | types: 7 | - completed 8 | branches: 9 | - main 10 | workflow_dispatch: 11 | 12 | jobs: 13 | copy-file: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout the repository 18 | uses: actions/checkout@v4 19 | with: 20 | ref: main 21 | 22 | - name: Set up Git 23 | run: | 24 | git config --global user.name 'github-actions[bot]' 25 | git config --global user.email 'github-actions[bot]@users.noreply.github.com' 26 | 27 | - name: Copy file to gh-pages branch 28 | run: | 29 | # Fetch and checkout the gh-pages branch 30 | git fetch origin gh-pages 31 | git checkout gh-pages 32 | 33 | # Copy the file from the main branch to the gh-pages branch 34 | git checkout main -- Assets/FidoKeys.json 35 | 36 | # Move the file to the desired location 37 | mv Assets/FidoKeys.json Explorer/FidoKeys.json 38 | 39 | # Commit and push the changes if there are any updates 40 | if [ -n "$(git status --porcelain)" ]; then 41 | git add Explorer/FidoKeys.json 42 | git commit -m "Update FidoKeys.json from main branch" 43 | git push origin gh-pages 44 | else 45 | echo "No changes to commit." 46 | fi 47 | -------------------------------------------------------------------------------- /Assets/valid_vendors.json: -------------------------------------------------------------------------------- 1 | { 2 | "vendors": [ 3 | "ACS", 4 | "Allthenticator", 5 | "Android", 6 | "Arculus", 7 | "AuthenTrend", 8 | "Atos", 9 | "authenton1", 10 | "Chipwon", 11 | "Chunghwa Telecom", 12 | "Crayonic", 13 | "Cryptnox", 14 | "Dapple Security", 15 | "Deepnet", 16 | "Egomet", 17 | "ellipticSecure", 18 | "Ensurity", 19 | "Eviden", 20 | "eWBM", 21 | "Excelsecu", 22 | "Feitian", 23 | "FIDO KeyPass", 24 | "Foongton", 25 | "FT-JCOS", 26 | "Goldkey", 27 | "Google", 28 | "GoTrust", 29 | "GSTAG", 30 | "HID", 31 | "HID Global", 32 | "Hideez", 33 | "Hypersecu", 34 | "HYPR", 35 | "ID-One", 36 | "IDCore", 37 | "IDEMIA", 38 | "Identiv", 39 | "IDmelon", 40 | "IIST", 41 | "ImproveID", 42 | "Infineon Technologies AG", 43 | "Kensington", 44 | "KEY-ID", 45 | "KeyXentic", 46 | "KeyVault", 47 | "KONAI", 48 | "Ledger", 49 | "NEOWAVE", 50 | "Nitrokey", 51 | "NXP Semiconductors", 52 | "Nymi", 53 | "OCTATCO", 54 | "OneKey", 55 | "OneSpan", 56 | "OnlyKey", 57 | "OpenSK", 58 | "Pone Biometrics", 59 | "Precision", 60 | "RSA", 61 | "SafeKey", 62 | "Samsung", 63 | "SafeNet", 64 | "Securité Carte à Puce", 65 | "Sentry Enterprises", 66 | "SHALO", 67 | "SI0X", 68 | "SmartDisplayer", 69 | "SoloKeys", 70 | "StarSign", 71 | "Swissbit", 72 | "T-Shield", 73 | "Taglio", 74 | "Thales", 75 | "Token Ring", 76 | "TOKEN2", 77 | "TruU", 78 | "VALMIDO", 79 | "Veridium", 80 | "VeridiumID", 81 | "VeroCard", 82 | "VinCSS", 83 | "VivoKey", 84 | "WinMagic", 85 | "WiSECURE", 86 | "Yubico", 87 | "ZTPass" 88 | ] 89 | } 90 | -------------------------------------------------------------------------------- /Scripts/Export-GHEntraFido.ps1: -------------------------------------------------------------------------------- 1 | Function Export-GHEntraFido { 2 | <# 3 | .SYNOPSIS 4 | Exports FIDO data from a specified URL. 5 | 6 | .DESCRIPTION 7 | This function fetches the HTML content from the provided URL, parses the HTML to find a specific table with headers "Description" and "AAGUID", 8 | and extracts the data from the table. The data is then output in a structured format. 9 | 10 | .PARAMETER Url 11 | The URL of the webpage containing the FIDO data table. 12 | 13 | .EXAMPLE 14 | Export-GHEntraFido -Url "https://example.com/fido-data" 15 | This command fetches and exports the FIDO data from the specified URL. 16 | 17 | .NOTES 18 | The function uses HtmlAgilityPack to parse the HTML content. 19 | #> 20 | [CmdletBinding()] 21 | param ( 22 | [Parameter(Mandatory = $true)] 23 | [string]$Url 24 | ) 25 | 26 | # Fetch the webpage content 27 | $response = Invoke-WebRequest -Uri $Url -UseBasicParsing 28 | $htmlContent = $response.Content 29 | 30 | # Load the HTML content into an HtmlDocument object 31 | $htmlDocument = New-Object HtmlAgilityPack.HtmlDocument 32 | $htmlDocument.LoadHtml($htmlContent) 33 | 34 | # Extract all table nodes from the HTML document 35 | $tableNodes = $htmlDocument.DocumentNode.SelectNodes("//table") 36 | 37 | # Find the target table based on headers 38 | $targetTableNode = $null 39 | foreach ($tableNode in $tableNodes) { 40 | $headers = $tableNode.SelectNodes(".//thead/tr/th") | ForEach-Object { $_.InnerText.Trim() } 41 | if ($headers -contains "Description" -and $headers -contains "AAGUID") { 42 | $targetTableNode = $tableNode 43 | break 44 | } 45 | } 46 | 47 | if ($null -ne $targetTableNode) { 48 | # Extract headers from the target table 49 | $headers = $targetTableNode.SelectNodes(".//thead/tr/th") | ForEach-Object { $_.InnerText.Trim() } 50 | 51 | # Initialize an array to hold the data 52 | $data = @() 53 | 54 | # Process the rows of the target table 55 | $rowNodes = $targetTableNode.SelectNodes(".//tbody/tr") 56 | foreach ($rowNode in $rowNodes) { 57 | $cellNodes = $rowNode.SelectNodes("td") 58 | $row = @{ 59 | Description = "" 60 | AAGUID = "" 61 | Bio = "" 62 | USB = "" 63 | NFC = "" 64 | BLE = "" 65 | Vendor = "" # Initialize Vendor as an empty string 66 | } 67 | 68 | for ($i = 0; $i -lt $cellNodes.Count; $i++) { 69 | $header = $headers[$i] 70 | $cell = $cellNodes[$i] 71 | $value = $cell.InnerText.Trim() 72 | 73 | if ($header -in @("Bio", "USB", "BLE", "NFC")) { 74 | $urlValue = $null 75 | if ($cell.SelectSingleNode(".//a")) { 76 | $urlValue = $cell.SelectSingleNode(".//a").GetAttributeValue("href", "") 77 | } elseif ($cell.SelectSingleNode(".//img")) { 78 | $urlValue = $cell.SelectSingleNode(".//img").GetAttributeValue("src", "") 79 | } 80 | 81 | if ($urlValue -match "(yes|no)\.png$") { 82 | $value = $matches[1] -replace "yes", "Yes" -replace "no", "No" 83 | } 84 | } 85 | 86 | $row[$header] = $value 87 | } 88 | 89 | $data += [PSCustomObject]$row 90 | } 91 | 92 | # Output the data in the specified order 93 | $data | Select-Object Description, AAGUID, Bio, USB, NFC, BLE, Vendor 94 | } else { 95 | Write-Error "No table found with the specified headers." 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Functions/Private/Export-EntraFido.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Fetches and processes FIDO2 hardware vendor data from a specified URL. 4 | 5 | .DESCRIPTION 6 | This function fetches the webpage content from the specified URL, parses the HTML to find the table with headers "Description" and "AAGUID", 7 | and processes the rows to extract data. It ensures the table order is "Description", "AAGUID", "Bio", "USB", "NFC", "BLE" and outputs the data 8 | as an object for easy use in the pipeline. 9 | 10 | .PARAMETER Url 11 | The URL of the webpage to fetch the FIDO2 hardware vendor data from. 12 | 13 | .EXAMPLE 14 | $data = Export-EntraFido -Url "https://learn.microsoft.com/en-us/entra/identity/authentication/concept-fido2-hardware-vendor" 15 | $data | Format-Table 16 | 17 | .EXAMPLE 18 | Export-EntraFido -Url "https://learn.microsoft.com/en-us/entra/identity/authentication/concept-fido2-hardware-vendor" | Export-Csv -Path "FidoData.csv" -NoTypeInformation 19 | #> 20 | function Export-EntraFido { 21 | [CmdletBinding()] 22 | param ( 23 | [Parameter(Mandatory = $true)] 24 | [string]$Url 25 | ) 26 | 27 | # Fetch the webpage content 28 | $response = Invoke-WebRequest -Uri $Url 29 | $htmlContent = $response.Content 30 | 31 | # Load the HTML content into an HtmlDocument object 32 | $htmlDocument = New-Object HtmlAgilityPack.HtmlDocument 33 | $htmlDocument.LoadHtml($htmlContent) 34 | 35 | # Extract all table nodes from the HTML document 36 | $tableNodes = $htmlDocument.DocumentNode.SelectNodes("//table") 37 | 38 | # Find the target table based on headers 39 | $targetTableNode = $null 40 | foreach ($tableNode in $tableNodes) { 41 | $headers = $tableNode.SelectNodes(".//thead/tr/th") | ForEach-Object { $_.InnerText.Trim() } 42 | if ($headers -contains "Description" -and $headers -contains "AAGUID") { 43 | $targetTableNode = $tableNode 44 | break 45 | } 46 | } 47 | 48 | if ($null -ne $targetTableNode) { 49 | # Extract headers from the target table 50 | $headers = $targetTableNode.SelectNodes(".//thead/tr/th") | ForEach-Object { $_.InnerText.Trim() } 51 | 52 | # Initialize an array to hold the data 53 | $data = @() 54 | 55 | # Process the rows of the target table 56 | $rowNodes = $targetTableNode.SelectNodes(".//tbody/tr") 57 | foreach ($rowNode in $rowNodes) { 58 | $cellNodes = $rowNode.SelectNodes("td") 59 | $row = @{ 60 | Description = "" 61 | AAGUID = "" 62 | Bio = "" 63 | USB = "" 64 | NFC = "" 65 | BLE = "" 66 | } 67 | 68 | for ($i = 0; $i -lt $cellNodes.Count; $i++) { 69 | $header = $headers[$i] 70 | $cell = $cellNodes[$i] 71 | $value = $cell.InnerText.Trim() 72 | 73 | if ($header -in @("Bio", "USB", "BLE", "NFC")) { 74 | $urlValue = $null 75 | if ($cell.SelectSingleNode(".//a")) { 76 | $urlValue = $cell.SelectSingleNode(".//a").GetAttributeValue("href", "") 77 | } elseif ($cell.SelectSingleNode(".//img")) { 78 | $urlValue = $cell.SelectSingleNode(".//img").GetAttributeValue("src", "") 79 | } 80 | 81 | if ($urlValue -match "(yes|no)\.png$") { 82 | $value = $matches[1] -replace "yes", "Yes" -replace "no", "No" 83 | } 84 | } 85 | 86 | $row[$header] = $value 87 | } 88 | 89 | $data += [PSCustomObject]$row 90 | } 91 | 92 | # Output the data in the specified order 93 | $data | Select-Object Description, AAGUID, Bio, USB, NFC, BLE 94 | } else { 95 | Write-Error "No table found with the specified headers." 96 | } 97 | } -------------------------------------------------------------------------------- /Functions/Public/Get-FIDODbLog.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Fetches and parses a FIDO database log from a specified URL. 4 | 5 | .DESCRIPTION 6 | The Get-FIDODbLog function retrieves a log file from a given URL and parses its content. 7 | The log entries are expected to follow a specific format, with each entry starting with a date and time stamp. 8 | Entries are categorized by lines starting with "Duplicate" or "Updated". The function returns the parsed log entries as an array of PSCustomObject. 9 | 10 | .PARAMETER Url 11 | The URL from which to fetch the log file. Defaults to "https://raw.githubusercontent.com/DevClate/EntraFIDOFinder/main/merge_log.md". 12 | 13 | .EXAMPLE 14 | PS> Get-FIDODbLog 15 | Fetches the log file from the default URL and displays the parsed log entries. 16 | 17 | .EXAMPLE 18 | PS> Get-FIDODbLog -Url "https://example.com/path/to/log.md" 19 | Fetches the log file from the specified URL and displays the parsed log entries. 20 | 21 | .NOTES 22 | Author: DevClate 23 | Date: 2024-10-12 24 | #> 25 | function Get-FIDODbLog { 26 | [CmdletBinding()] 27 | param ( 28 | [string]$Url = "https://raw.githubusercontent.com/DevClate/EntraFIDOFinder/main/merge_log.md" 29 | ) 30 | 31 | try { 32 | # Fetching log file from URL 33 | $logContent = Invoke-RestMethod -Uri $Url 34 | 35 | if ($logContent) { 36 | # Split the log content into lines 37 | $logLines = $logContent -split "`n" 38 | 39 | # Initialize variables 40 | $logs = @() 41 | $currentDate = $null 42 | $currentEntry = "" 43 | 44 | # Parse the log lines 45 | foreach ($line in $logLines) { 46 | if ($line -match "^# Merge Log - (\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})$") { 47 | # New log submission date and time 48 | if ($currentDate) { 49 | if ($currentEntry -ne "") { 50 | $logs += [PSCustomObject]@{ 51 | Date = $currentDate 52 | Entry = $currentEntry.Trim() 53 | } 54 | $currentEntry = "" 55 | } 56 | } 57 | $currentDate = $matches[1] 58 | } elseif ($line -eq "") { 59 | # Blank line indicates a new log entry 60 | if ($currentEntry -ne "") { 61 | $logs += [PSCustomObject]@{ 62 | Date = $currentDate 63 | Entry = $currentEntry.Trim() 64 | } 65 | $currentEntry = "" 66 | } 67 | } elseif ($line -match "^(Duplicate|Updated)") { 68 | # New log entry for lines starting with "Duplicate" or "Updated" 69 | if ($currentEntry -ne "") { 70 | $logs += [PSCustomObject]@{ 71 | Date = $currentDate 72 | Entry = $currentEntry.Trim() 73 | } 74 | $currentEntry = "" 75 | } 76 | $currentEntry += "$line`n" 77 | } else { 78 | # Append line to the current entry 79 | $currentEntry += "$line`n" 80 | } 81 | } 82 | 83 | # Add the last log entry 84 | if ($currentDate -and $currentEntry -ne "") { 85 | $logs += [PSCustomObject]@{ 86 | Date = $currentDate 87 | Entry = $currentEntry.Trim() 88 | } 89 | } 90 | 91 | # Return the logs as a PSCustomObject array and format as list 92 | $logs | Format-List 93 | } else { 94 | Write-Host "No content found at the specified URL." -ForegroundColor Yellow 95 | } 96 | } catch { 97 | Write-Host "An error occurred while fetching the log file: $_" -ForegroundColor Red 98 | } 99 | } -------------------------------------------------------------------------------- /Functions/Public/Show-FIDODbVersion.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Displays the last updated date of the FIDO database from a specified JSON file and checks if it is the most current version. 4 | 5 | .DESCRIPTION 6 | The Show-FIDODbVersion function retrieves and displays the last updated date of the FIDO database from a JSON file. 7 | The JSON file can be specified by a file path. If no file path is provided, the function attempts to locate the JSON file in a default directory. 8 | The function also downloads the latest version of the JSON file from a predefined URL and compares the dates to inform the user if their local version is the most current. 9 | 10 | .PARAMETER JsonFilePath 11 | Specifies the path to the JSON file containing the FIDO database metadata. This parameter is optional. 12 | 13 | .EXAMPLE 14 | Show-FIDODbVersion -JsonFilePath "C:\Path\To\FidoKeys.json" 15 | Displays the last updated date of the FIDO database from the specified JSON file and checks if it is the most current version. 16 | 17 | .EXAMPLE 18 | Show-FIDODbVersion 19 | Attempts to locate the JSON file in a default directory, displays the last updated date of the FIDO database, and checks if it is the most current version. 20 | 21 | .NOTES 22 | If the JsonFilePath parameter is omitted, the function attempts to locate the JSON file in a default directory relative to the script's location. 23 | 24 | #> 25 | 26 | function Show-FIDODbVersion { 27 | [CmdletBinding()] 28 | param ( 29 | [Parameter(Mandatory = $false)] 30 | [string]$JsonFilePath 31 | ) 32 | 33 | # Default URL for the latest JSON file 34 | $latestJsonUrl = "https://raw.githubusercontent.com/DevClate/EntraFIDOFinder/main/Assets/FidoKeys.json" 35 | 36 | # Determine the JSON file path 37 | if (-not $JsonFilePath) { 38 | $parentDir = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent 39 | $JsonFilePath = Join-Path -Path $parentDir -ChildPath "Assets/FidoKeys.json" 40 | } 41 | 42 | # Check if the local JSON file exists 43 | if (-Not (Test-Path -Path $JsonFilePath)) { 44 | Write-Error "The JSON file was not found at path: $JsonFilePath" 45 | return 46 | } 47 | 48 | # Read the local JSON file 49 | $localJsonData = Get-Content -Raw -Path $JsonFilePath | ConvertFrom-Json 50 | 51 | # Check if the metadata and databaseLastUpdated fields exist in the local JSON file 52 | if ($null -eq $localJsonData.metadata -or $null -eq $localJsonData.metadata.databaseLastUpdated) { 53 | Write-Error "The local JSON file does not contain the required metadata or databaseLastUpdated fields." 54 | return 55 | } 56 | 57 | # Display the last updated date of the local JSON file 58 | $localLastUpdated = [datetime]::ParseExact($localJsonData.metadata.databaseLastUpdated, "yyyy-MM-dd HH:mm:ss", $null) 59 | Write-Output ("The local database was last updated on: {0:yyyy-MM-dd HH:mm:ss}" -f $localLastUpdated) 60 | 61 | # Fetch the latest JSON file from the URL 62 | try { 63 | $latestJsonData = Invoke-RestMethod -Uri $latestJsonUrl 64 | } catch { 65 | Write-Error "Failed to download the latest JSON file from URL: $latestJsonUrl" 66 | return 67 | } 68 | 69 | # Check if the metadata and databaseLastUpdated fields exist in the latest JSON file 70 | if ($null -eq $latestJsonData.metadata -or $null -eq $latestJsonData.metadata.databaseLastUpdated) { 71 | Write-Error "The latest JSON file does not contain the required metadata or databaseLastUpdated fields." 72 | return 73 | } 74 | 75 | # Get the last updated date of the latest JSON file 76 | $latestLastUpdated = [datetime]::ParseExact($latestJsonData.metadata.databaseLastUpdated, "yyyy-MM-dd HH:mm:ss", $null) 77 | 78 | # Compare the dates and inform the user 79 | if ($localLastUpdated -eq $latestLastUpdated) { 80 | Write-Output "Your local database is up to date." 81 | } else { 82 | Write-Output ("A newer version of the database is available. The latest database was last updated on: {0:yyyy-MM-dd HH:mm:ss}" -f $latestLastUpdated) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /EntraFIDOFinder.psd1: -------------------------------------------------------------------------------- 1 | # 2 | # Module manifest for module 'EntraFIDOFinder' 3 | # 4 | # Generated by: Clayton Tyger 5 | # 6 | # Generated on: 9/30/2024 7 | # 8 | 9 | @{ 10 | 11 | # Script module or binary module file associated with this manifest. 12 | RootModule = 'EntraFIDOFinder.psm1' 13 | 14 | # Version number of this module. 15 | ModuleVersion = '0.0.20' 16 | 17 | # Supported PSEditions 18 | CompatiblePSEditions = 'Core' 19 | 20 | # ID used to uniquely identify this module 21 | GUID = 'baadf934-f5a1-4978-b5a8-710dfb135a2e' 22 | 23 | # Author of this module 24 | Author = 'Clayton Tyger' 25 | 26 | # Company or vendor of this module 27 | CompanyName = 'Clayton Tyger' 28 | 29 | # Copyright statement for this module 30 | Copyright = '(c) Clayton Tyger. All rights reserved.' 31 | 32 | # Description of the functionality provided by this module 33 | Description = 'PowerShell Module to find compatible attestation FIDO2 keys for Entra.' 34 | 35 | # Minimum version of the PowerShell engine required by this module 36 | PowerShellVersion = '7.0' 37 | 38 | # Name of the PowerShell host required by this module 39 | # PowerShellHostName = '' 40 | 41 | # Minimum version of the PowerShell host required by this module 42 | # PowerShellHostVersion = '' 43 | 44 | # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 45 | # DotNetFrameworkVersion = '' 46 | 47 | # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 48 | # ClrVersion = '' 49 | 50 | # Processor architecture (None, X86, Amd64) required by this module 51 | # ProcessorArchitecture = '' 52 | 53 | # Modules that must be imported into the global environment prior to importing this module 54 | #RequiredModules = @() 55 | 56 | # Assemblies that must be loaded prior to importing this module 57 | # RequiredAssemblies = @() 58 | 59 | # Script files (.ps1) that are run in the caller's environment prior to importing this module. 60 | # ScriptsToProcess = @() 61 | 62 | # Type files (.ps1xml) to be loaded when importing this module 63 | # TypesToProcess = @() 64 | 65 | # Format files (.ps1xml) to be loaded when importing this module 66 | # FormatsToProcess = @() 67 | 68 | # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess 69 | # NestedModules = @() 70 | 71 | # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. 72 | FunctionsToExport = @( 73 | 'Find-FIDOKey', 74 | 'Show-FIDODbVersion', 75 | 'Get-FIDODbLog' 76 | ) 77 | 78 | # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. 79 | CmdletsToExport = @() 80 | 81 | # Variables to export from this module 82 | VariablesToExport = @() 83 | 84 | # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. 85 | AliasesToExport = @() 86 | 87 | # DSC resources to export from this module 88 | # DscResourcesToExport = @() 89 | 90 | # List of all modules packaged with this module 91 | # ModuleList = @() 92 | 93 | # List of all files packaged with this module 94 | # FileList = @() 95 | 96 | # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. 97 | PrivateData = @{ 98 | 99 | PSData = @{ 100 | 101 | # Tags applied to this module. These help with module discovery in online galleries. 102 | Tags = @('Entra', '365', 'FIDO', 'Security', 'SSO', 'MFA', 'Microsoft', 'MacOS', 'Windows', 'PSEdition_Core') 103 | 104 | # A URL to the license for this module. 105 | # LicenseUri = '' 106 | 107 | # A URL to the main website for this project. 108 | ProjectUri = 'https://github.com/DevClate/EntraFIDOFinder' 109 | 110 | # A URL to an icon representing this module. 111 | # IconUri = '' 112 | 113 | # ReleaseNotes of this module 114 | # ReleaseNotes = '' 115 | 116 | # Prerelease string of this module 117 | # Prerelease = '' 118 | 119 | # Flag to indicate whether the module requires explicit user acceptance for install/update/save 120 | # RequireLicenseAcceptance = $false 121 | 122 | # External dependent modules of this module 123 | # ExternalModuleDependencies = @() 124 | 125 | } # End of PSData hashtable 126 | 127 | } # End of PrivateData hashtable 128 | 129 | # HelpInfo URI of this module 130 | # HelpInfoURI = '' 131 | 132 | # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. 133 | # DefaultCommandPrefix = '' 134 | 135 | } 136 | 137 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EntraFIDOFinder  [](https://github.com/DevClate/EntraFIDOFinder) [](https://github.com/DevClate/EntraFIDOFinder) 2 | 3 | PowerShell Module to find compatible attestation FIDO2 keys for Entra. 4 | 5 | **Database Last Updated:** 2025-10-19 6 | 7 | ## Features 8 | 9 | * View and filter standard properties 10 | * See all properties in nicely formatted JSON 11 | * Import AAGUIDs from .TXT, .CSV, and .XLSX file 12 | * Accepts pipeline input for AAGUIDs 13 | * See all standard properties then pick out which properties you want to see - For some of the properties you will have to format because of nesting 14 | * Merge FIDO Alliance and Microsoft Data together through GitHub Action 15 | * Web Version to see and filter all AAGUIDs 16 | 17 | ## Getting Started 18 | 19 | ```PowerShell 20 | # Run first if you need to set Execution Policy 21 | Set-ExecutionPolicy RemoteSigned -Scope CurrentUser 22 | 23 | # Install EntraFIDOFinder 24 | Install-PSResource -Name EntraFIDOFinder -Scope CurrentUser 25 | ``` 26 | 27 | ## Starter Cmdlets 28 | 29 | ```powershell 30 | # Find all compatible keys 31 | Find-FIDOKey 32 | 33 | # Find all Yubico keys 34 | Find-FIDOKey -Brand Yubico 35 | 36 | # Find all Yubico and OneSpan 37 | Find-FIDOKey -Brand Yubico,Onespan 38 | 39 | # Find keys that have both USB and NFC 40 | Find-FIDOKey -Type USB,NFC -TypeFilterMode AtLeastTwo 41 | 42 | # Find keys that have either USB or NFC 43 | Find-FIDOKey -Type USB,NFC 44 | 45 | # Find keys that are FIDO 2.1 46 | Find-FIDOKey -FIDOVersion "FIDO 2.1" 47 | 48 | # Find keys that are FIDO 2.1 with all properties in JSON output 49 | Find-FIDOKey -FIDOVersion "FIDO 2.1" -AllProperties 50 | 51 | # Here is an example showing the standard with Protocol Family 52 | Find-FIDOKey -DetailedProperties | Select-Object Vendor, Description, @{Name="ProtocolFamily";Expression={$_.metadataStatement.protocolFamily}} | fl 53 | 54 | # Find information on a single AAGUID 55 | "50a45b0c-80e7-f944-bf29-f552bfa2e048" | Find-FIDOKey 56 | 57 | # Find information on more than 1 AAGUID 58 | "973446ca-e21c-9a9b-99f5-9b985a67af0f", "50a45b0c-80e7-f944-bf29-f552bfa2e048" | Find-FIDOKey 59 | 60 | # Import multiple AAGUID from a file (.xlsx, .csv, and .txt) 61 | Find-FIDOKey -AAGUIDFile "aaguid.xlsx" 62 | 63 | # Find your databse version and compare to newest version 64 | Show-FIDODbVersion 65 | 66 | # View Master Database Log 67 | Get-FIDODbLog 68 | ``` 69 | 70 | ## Properties 71 | 72 | Brands: 73 | This parameter is validated so if you start typing in a brand and press tab it will fill the rest of the brand name in if it is available. 74 | 75 | Type: 76 | The four types of keys are USB, NFC, BIO, and BLE which are also validated in so you can tab complete. 77 | 78 | AllProperties: 79 | Shows all properties from the FIDO Alliance in JSON format 80 | 81 | DetailedProperties: 82 | Allows you to pull any property from the FIDO Alliance, but some you may have to play with depending on how nested they are. 83 | 84 | ## Sample Outputs 85 | 86 | ```Powershell 87 | "50a45b0c-80e7-f944-bf29-f552bfa2e048", "973446ca-e21c-9a9b-99f5-9b985a67af0f" | Find-FIDOKey 88 | 89 | Vendor : ACS 90 | Description : ACS FIDO Authenticator 91 | AAGUID : 50a45b0c-80e7-f944-bf29-f552bfa2e048 92 | Bio : No 93 | USB : Yes 94 | NFC : No 95 | BLE : No 96 | Version : FIDO 2.1 PRE 97 | ValidVendor : Yes 98 | 99 | Vendor : ACS 100 | Description : ACS FIDO Authenticator Card 101 | AAGUID : 973446ca-e21c-9a9b-99f5-9b985a67af0f 102 | Bio : No 103 | USB : No 104 | NFC : Yes 105 | BLE : No 106 | Version : FIDO 2.1 PRE 107 | ValidVendor : Yes 108 | 109 | "50a45b0c-80e7-f944-bf29-f552bfa2e048" | Find-FIDOKey -AllProperties 110 | { 111 | "Vendor": "ACS", 112 | "Description": "ACS FIDO Authenticator", 113 | "AAGUID": "50a45b0c-80e7-f944-bf29-f552bfa2e048", 114 | "Bio": "No", 115 | "USB": "Yes", 116 | "NFC": "No", 117 | "BLE": "No", 118 | "Version": "FIDO 2.1 PRE", 119 | "ValidVendor": "Yes", 120 | "metadataStatement": { 121 | "legalHeader": "Submission of this statement and retrieval and use of this statement indicates acceptance of the appropriate agreement located at https://fidoalliance.org/metadata/metadata-legal-terms/.", 122 | "aaguid": "50a45b0c-80e7-f944-bf29-f552bfa2e048", 123 | "description": "ACS FIDO Authenticator", 124 | "authenticatorVersion": 10000, 125 | "protocolFamily": "fido2", 126 | "schema": 3, 127 | "upv": [ 128 | { 129 | "major": 1, 130 | "minor": 1 131 | }, 132 | { 133 | "major": 1, 134 | "minor": 0 135 | } 136 | ], and more data below 137 | ``` 138 | 139 | If you are curious on the FIDO Alliance data, I've now added that into the metadata and it will be compared once a month when the FIDO Alliance publishes the newest version. It is accessible in the PowerShell version using the -AllProperties parameter, or in the web version by clicking on the actual key, then clicking on "Show Raw Data." 140 | 141 | ## Web Version 142 | 143 | [https://devclate.github.io/EntraFIDOFinder/Explorer/](https://devclate.github.io/EntraFIDOFinder/Explorer/) 144 |  145 | -------------------------------------------------------------------------------- /Scripts/Test-GHValidVendor.ps1: -------------------------------------------------------------------------------- 1 | function Test-GHValidVendor { 2 | <# 3 | .SYNOPSIS 4 | Tests if a vendor is valid based on a list of valid vendors. 5 | 6 | .DESCRIPTION 7 | This function checks if the provided vendor is in the list of valid vendors. If not, it attempts to use the first word of the description as the vendor. 8 | If the vendor is still invalid and it's a new entry, it logs the invalid vendor and prepares an issue entry. 9 | 10 | .PARAMETER vendor 11 | [ref] The vendor to be validated. 12 | 13 | .PARAMETER description 14 | [string] The description associated with the vendor. 15 | 16 | .PARAMETER aaguid 17 | [string] The AAGUID associated with the vendor. 18 | 19 | .PARAMETER ValidVendors 20 | [string[]] The list of valid vendors. 21 | 22 | .PARAMETER markdownContent 23 | [System.Collections.ArrayList] The collection to store markdown log entries. 24 | 25 | .PARAMETER detailedLogContent 26 | [System.Collections.ArrayList] The collection to store detailed log entries. 27 | 28 | .PARAMETER loggedInvalidVendors 29 | [System.Collections.ArrayList] The collection to store logged invalid vendors. 30 | 31 | .PARAMETER issueEntries 32 | [System.Collections.ArrayList] The collection to store issue entries. 33 | 34 | .PARAMETER existingLogEntries 35 | [string[]] The list of existing log entries. 36 | 37 | .PARAMETER changesDetected 38 | [ref] Indicates if any changes were detected. 39 | 40 | .PARAMETER IsNewEntry 41 | [bool] Indicates if the entry is new. 42 | 43 | .PARAMETER currentLogEntries 44 | [System.Collections.ArrayList] The collection to store current log entries. 45 | 46 | .OUTPUTS 47 | [string] Returns "Yes" if the vendor is valid or corrected, otherwise returns "No". 48 | 49 | .EXAMPLE 50 | $vendor = [ref] "SomeVendor" 51 | $description = "Some description" 52 | $aaguid = "1234-5678-9012" 53 | $ValidVendors = @("ValidVendor1", "ValidVendor2") 54 | $markdownContent = [System.Collections.ArrayList]::new() 55 | $detailedLogContent = [System.Collections.ArrayList]::new() 56 | $loggedInvalidVendors = [System.Collections.ArrayList]::new() 57 | $issueEntries = [System.Collections.ArrayList]::new() 58 | $existingLogEntries = @() 59 | $changesDetected = [ref] $false 60 | $IsNewEntry = $true 61 | $currentLogEntries = [System.Collections.ArrayList]::new() 62 | 63 | Test-GHValidVendor -vendor $vendor -description $description -aaguid $aaguid -ValidVendors $ValidVendors -markdownContent $markdownContent -detailedLogContent $detailedLogContent -loggedInvalidVendors $loggedInvalidVendors -issueEntries $issueEntries -existingLogEntries $existingLogEntries -changesDetected $changesDetected -IsNewEntry $IsNewEntry -currentLogEntries $currentLogEntries 64 | #> 65 | 66 | param ( 67 | [Parameter(Mandatory = $true)] 68 | [ref]$vendor, 69 | [Parameter(Mandatory = $true)] 70 | [string]$description, 71 | [Parameter(Mandatory = $true)] 72 | [string]$aaguid, 73 | [string[]]$ValidVendors, 74 | [System.Collections.ArrayList]$markdownContent, 75 | [System.Collections.ArrayList]$detailedLogContent, 76 | [System.Collections.ArrayList]$loggedInvalidVendors, 77 | [System.Collections.ArrayList]$issueEntries, 78 | [string[]]$existingLogEntries, 79 | [ref]$changesDetected, 80 | [bool]$IsNewEntry, 81 | [System.Collections.ArrayList]$currentLogEntries 82 | ) 83 | 84 | # Check if the vendor is valid 85 | if ($ValidVendors -contains $vendor.Value) { 86 | return "Yes" 87 | } 88 | else { 89 | # Attempt to use the first word of the description as the vendor 90 | $firstWord = ($description -split ' ')[0] 91 | if ($firstWord -and $firstWord -ne $vendor.Value) { 92 | Write-Host "Vendor '$($vendor.Value)' is invalid. Trying first word of description '$firstWord' as vendor." 93 | if ($ValidVendors -contains $firstWord) { 94 | # Update vendor to first word and return Yes 95 | Write-Host "Vendor '$firstWord' is valid." 96 | $oldVendor = $vendor.Value 97 | $vendor.Value = $firstWord 98 | $logEntry = "Vendor corrected for AAGUID '$aaguid': '$oldVendor' to '$firstWord'." 99 | $detailedLogContent.Add("") 100 | $detailedLogContent.Add($logEntry) 101 | Write-Host "Added log entry for vendor correction: $logEntry" 102 | $changesDetected.Value = $true 103 | 104 | # Add the log entry to current log entries 105 | $currentLogEntries.Add($logEntry) 106 | 107 | return "Yes" 108 | } 109 | } 110 | 111 | # Try to find any valid vendor in the description 112 | foreach ($validVendorName in $ValidVendors) { 113 | if ($description -match $validVendorName) { 114 | Write-Host "Found valid vendor '$validVendorName' in description for AAGUID '$aaguid'." 115 | $oldVendor = $vendor.Value 116 | $vendor.Value = $validVendorName 117 | $logEntry = "Vendor updated for AAGUID '$aaguid': '$oldVendor' to '$validVendorName' from description match." 118 | $detailedLogContent.Add("") 119 | $detailedLogContent.Add($logEntry) 120 | Write-Host "Added log entry for vendor correction: $logEntry" 121 | $changesDetected.Value = $true 122 | 123 | # Add the log entry to current log entries 124 | $currentLogEntries.Add($logEntry) 125 | 126 | return "Yes" 127 | } 128 | } 129 | 130 | # Log invalid vendor for the specific key if it's a new entry 131 | if ($IsNewEntry) { 132 | $logEntry = "Invalid vendor detected for AAGUID '$aaguid' with description '$description'. Vendor '$($vendor.Value)' is not in the list of valid vendors." 133 | $logEntryTrimmed = $logEntry.Trim() 134 | 135 | if (-not ($existingLogEntries -contains $logEntryTrimmed)) { 136 | $markdownContent.Add($logEntry) 137 | $detailedLogContent.Add("") 138 | $detailedLogContent.Add($logEntry) 139 | Write-Host "Added log entry for invalid vendor: $logEntry" 140 | 141 | # Add log entry to currentLogEntries 142 | $currentLogEntries.Add($logEntry) 143 | 144 | # Set changesDetected to true 145 | $changesDetected.Value = $true 146 | 147 | # Prepare issue entry with AAGUID included in the title 148 | $issueTitle = "Invalid Vendor Detected for AAGUID $aaguid : $($vendor.Value)" 149 | $issueBody = $logEntry 150 | $issueEntries.Add("$issueTitle|$issueBody|InvalidVendor") 151 | } 152 | } 153 | return "No" 154 | } 155 | } -------------------------------------------------------------------------------- /.github/workflows/GHMerge.yml: -------------------------------------------------------------------------------- 1 | name: Main - Merge 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: '0 0 * * *' 7 | push: 8 | branches: 9 | - main 10 | pull_request: 11 | branches: 12 | - main 13 | 14 | jobs: 15 | merge-fido-data: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: Checkout repository 20 | uses: actions/checkout@v4 21 | with: 22 | fetch-depth: 0 23 | ref: main 24 | 25 | - name: Install PSParseHTML Module 26 | shell: pwsh 27 | run: Install-Module -Name PSParseHTML -Force -Scope CurrentUser 28 | 29 | - name: Run Merge-GHFidoData Script 30 | id: merge_script 31 | shell: pwsh 32 | env: 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | GITHUB_REPOSITORY: ${{ github.repository }} 35 | run: | 36 | Import-Module PSParseHTML 37 | . ./Scripts/Test-GHValidVendor.ps1 38 | . ./Scripts/Test-GHAAGUIDExists.ps1 39 | . ./Scripts/Export-GHEntraFido.ps1 40 | . ./Scripts/Merge-GHFidoData.ps1 41 | 42 | - name: Read Environment Variables 43 | shell: bash 44 | run: | 45 | if [ -f ./Scripts/env_vars.txt ]; then 46 | echo "Setting environment variables from env_vars.txt" 47 | cat ./Scripts/env_vars.txt >> $GITHUB_ENV 48 | else 49 | echo "env_vars.txt not found." 50 | fi 51 | 52 | - name: Debug - Display ISSUE_ENTRIES, KEYS_NOW_VALID, and VENDORS_NOW_VALID Environment Variables 53 | shell: bash 54 | run: | 55 | echo "ISSUE_ENTRIES: $ISSUE_ENTRIES" 56 | echo "KEYS_NOW_VALID: $KEYS_NOW_VALID" 57 | echo "VENDORS_NOW_VALID: $VENDORS_NOW_VALID" 58 | 59 | - name: Close Fixed Issues and Create New Issues 60 | uses: actions/github-script@v6 61 | with: 62 | github-token: ${{ secrets.GITHUB_TOKEN }} 63 | script: | 64 | const issueEntriesRaw = process.env.ISSUE_ENTRIES || ''; 65 | const issueEntries = issueEntriesRaw.split('%0A').map(entry => decodeURIComponent(entry)).filter(entry => entry.trim() !== ''); 66 | if (issueEntries.length === 0) { 67 | console.log('No new issue entries found.'); 68 | } else { 69 | for (const entry of issueEntries) { 70 | const parts = entry.split('|'); 71 | if (parts.length < 2) { 72 | console.error(`Invalid entry format: ${entry}`); 73 | continue; 74 | } 75 | const [issueTitle, issueBody, issueLabel] = parts; 76 | console.log(`Processing issue: ${issueTitle}`); 77 | const { data: issues } = await github.rest.issues.listForRepo({ 78 | owner: context.repo.owner, 79 | repo: context.repo.repo, 80 | state: 'open', 81 | labels: 'auto-generated', 82 | }); 83 | const existingIssue = issues.find(issue => issue.title === issueTitle); 84 | if (!existingIssue) { 85 | const assignees = []; 86 | if (issueLabel === 'InvalidVendor' || issueLabel === 'DuplicateEntry') { 87 | assignees.push('DevClate'); 88 | } 89 | await github.rest.issues.create({ 90 | owner: context.repo.owner, 91 | repo: context.repo.repo, 92 | title: issueTitle, 93 | body: issueBody, 94 | labels: issueLabel ? ['auto-generated', issueLabel] : ['auto-generated'], 95 | assignees: assignees, 96 | }); 97 | console.log(`Issue created: ${issueTitle}`); 98 | } else { 99 | console.log(`Issue already exists: ${issueTitle}`); 100 | } 101 | } 102 | } 103 | 104 | // Close issues for keys (AAGUIDs) that are now valid 105 | const keysNowValidRaw = process.env.KEYS_NOW_VALID || ''; 106 | const keysNowValid = keysNowValidRaw.split('%0A').map(entry => decodeURIComponent(entry)).filter(entry => entry.trim() !== ''); 107 | if (keysNowValid.length === 0) { 108 | console.log('No keys have become valid.'); 109 | } else { 110 | console.log('Keys that are now valid:', keysNowValid); 111 | for (const aaguid of keysNowValid) { 112 | const { data: issues } = await github.rest.issues.listForRepo({ 113 | owner: context.repo.owner, 114 | repo: context.repo.repo, 115 | state: 'open', 116 | labels: ['auto-generated', 'InvalidVendor'], 117 | per_page: 100, 118 | }); 119 | for (const issue of issues) { 120 | if (issue.title.includes(aaguid)) { 121 | await github.rest.issues.update({ 122 | owner: context.repo.owner, 123 | repo: context.repo.repo, 124 | issue_number: issue.number, 125 | state: 'closed', 126 | state_reason: 'completed', 127 | }); 128 | await github.rest.issues.createComment({ 129 | owner: context.repo.owner, 130 | repo: context.repo.repo, 131 | issue_number: issue.number, 132 | body: `The vendor for key with AAGUID '${aaguid}' is now valid. This issue is being closed automatically.`, 133 | }); 134 | console.log(`Closed issue for key with AAGUID: ${aaguid}`); 135 | } 136 | } 137 | } 138 | } 139 | 140 | - name: Display Merge Log 141 | shell: bash 142 | run: | 143 | echo "Extracting newest 3 entries from merge_log.md" 144 | 145 | # Extract the first 3 '# Merge Log -' sections 146 | awk '/^# Merge Log -/{n++; if(n>3) exit} {print}' merge_log.md > newest_merge_log_entries.txt 147 | 148 | # Adjust header levels from '#' to '###' to fit within the summary 149 | sed 's/^# /### /' newest_merge_log_entries.txt > temp_merge_log_entries.txt 150 | 151 | # Append to GitHub Action Summary without code block 152 | echo "## Merge Log - Newest 3 Entries" >> $GITHUB_STEP_SUMMARY 153 | cat temp_merge_log_entries.txt >> $GITHUB_STEP_SUMMARY 154 | echo "" >> $GITHUB_STEP_SUMMARY 155 | 156 | # Clean up temporary files 157 | rm temp_merge_log_entries.txt newest_merge_log_entries.txt 158 | 159 | - name: Display Detailed Log 160 | shell: bash 161 | run: | 162 | echo "Extracting the 3 newest entries from detailed_log.txt" 163 | 164 | # Extract the first 3 'Detailed Log -' sections 165 | awk '/^Detailed Log -/{n++; if(n>3) exit} {print}' detailed_log.txt > newest_detailed_log_entries.txt 166 | 167 | # Append to GitHub Action Summary 168 | echo "## Detailed Log - Newest 3 Entries" >> $GITHUB_STEP_SUMMARY 169 | echo '```' >> $GITHUB_STEP_SUMMARY 170 | cat newest_detailed_log_entries.txt >> $GITHUB_STEP_SUMMARY 171 | echo '```' >> $GITHUB_STEP_SUMMARY 172 | 173 | # Clean up temporary file 174 | rm newest_detailed_log_entries.txt 175 | 176 | - name: Configure Git 177 | run: | 178 | git config --global user.name 'DevClate' 179 | git config --global user.email 'clate@clatent.com' 180 | 181 | - name: Commit changes 182 | run: | 183 | git add Assets/FidoKeys.json merge_log.md detailed_log.txt 184 | git commit -m "Update FidoKeys.json, merge_log.md, and detailed_log.txt" || echo "No changes to commit" 185 | 186 | - name: Push changes 187 | uses: ad-m/github-push-action@v0.6.0 188 | with: 189 | github_token: ${{ secrets.GITHUB_TOKEN }} 190 | branch: main 191 | -------------------------------------------------------------------------------- /.github/workflows/GHMergeDev.yml: -------------------------------------------------------------------------------- 1 | name: development - Merge 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: '0 0 * * *' 7 | push: 8 | branches: 9 | - development 10 | pull_request: 11 | branches: 12 | - development 13 | 14 | jobs: 15 | merge-fido-data: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: Checkout repository 20 | uses: actions/checkout@v4 21 | with: 22 | fetch-depth: 0 23 | ref: development 24 | 25 | - name: Install PSParseHTML Module 26 | shell: pwsh 27 | run: Install-Module -Name PSParseHTML -Force -Scope CurrentUser 28 | 29 | - name: Run Merge-GHFidoData Script 30 | id: merge_script 31 | shell: pwsh 32 | env: 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | GITHUB_REPOSITORY: ${{ github.repository }} 35 | run: | 36 | Import-Module PSParseHTML 37 | . ./Scripts/Test-GHValidVendor.ps1 38 | . ./Scripts/Test-GHAAGUIDExists.ps1 39 | . ./Scripts/Export-GHEntraFido.ps1 40 | . ./Scripts/Merge-GHFidoData.ps1 41 | 42 | - name: Read Environment Variables 43 | shell: bash 44 | run: | 45 | if [ -f ./Scripts/env_vars.txt ]; then 46 | echo "Setting environment variables from env_vars.txt" 47 | cat ./Scripts/env_vars.txt >> $GITHUB_ENV 48 | else 49 | echo "env_vars.txt not found." 50 | fi 51 | 52 | - name: Debug - Display ISSUE_ENTRIES, KEYS_NOW_VALID, and VENDORS_NOW_VALID Environment Variables 53 | shell: bash 54 | run: | 55 | echo "ISSUE_ENTRIES: $ISSUE_ENTRIES" 56 | echo "KEYS_NOW_VALID: $KEYS_NOW_VALID" 57 | echo "VENDORS_NOW_VALID: $VENDORS_NOW_VALID" 58 | 59 | - name: Close Fixed Issues and Create New Issues 60 | uses: actions/github-script@v6 61 | with: 62 | github-token: ${{ secrets.GITHUB_TOKEN }} 63 | script: | 64 | const issueEntriesRaw = process.env.ISSUE_ENTRIES || ''; 65 | const issueEntries = issueEntriesRaw.split('%0A').map(entry => decodeURIComponent(entry)).filter(entry => entry.trim() !== ''); 66 | if (issueEntries.length === 0) { 67 | console.log('No new issue entries found.'); 68 | } else { 69 | for (const entry of issueEntries) { 70 | const parts = entry.split('|'); 71 | if (parts.length < 2) { 72 | console.error(`Invalid entry format: ${entry}`); 73 | continue; 74 | } 75 | const [issueTitle, issueBody, issueLabel] = parts; 76 | console.log(`Processing issue: ${issueTitle}`); 77 | const { data: issues } = await github.rest.issues.listForRepo({ 78 | owner: context.repo.owner, 79 | repo: context.repo.repo, 80 | state: 'open', 81 | labels: 'auto-generated', 82 | }); 83 | const existingIssue = issues.find(issue => issue.title === issueTitle); 84 | if (!existingIssue) { 85 | const assignees = []; 86 | if (issueLabel === 'InvalidVendor' || issueLabel === 'DuplicateEntry') { 87 | assignees.push('DevClate'); 88 | } 89 | await github.rest.issues.create({ 90 | owner: context.repo.owner, 91 | repo: context.repo.repo, 92 | title: issueTitle, 93 | body: issueBody, 94 | labels: issueLabel ? ['auto-generated', issueLabel] : ['auto-generated'], 95 | assignees: assignees, 96 | }); 97 | console.log(`Issue created: ${issueTitle}`); 98 | } else { 99 | console.log(`Issue already exists: ${issueTitle}`); 100 | } 101 | } 102 | } 103 | 104 | // Close issues for keys (AAGUIDs) that are now valid 105 | const keysNowValidRaw = process.env.KEYS_NOW_VALID || ''; 106 | const keysNowValid = keysNowValidRaw.split('%0A').map(entry => decodeURIComponent(entry)).filter(entry => entry.trim() !== ''); 107 | if (keysNowValid.length === 0) { 108 | console.log('No keys have become valid.'); 109 | } else { 110 | console.log('Keys that are now valid:', keysNowValid); 111 | for (const aaguid of keysNowValid) { 112 | const { data: issues } = await github.rest.issues.listForRepo({ 113 | owner: context.repo.owner, 114 | repo: context.repo.repo, 115 | state: 'open', 116 | labels: ['auto-generated', 'InvalidVendor'], 117 | per_page: 100, 118 | }); 119 | for (const issue of issues) { 120 | if (issue.title.includes(aaguid)) { 121 | await github.rest.issues.update({ 122 | owner: context.repo.owner, 123 | repo: context.repo.repo, 124 | issue_number: issue.number, 125 | state: 'closed', 126 | state_reason: 'completed', 127 | }); 128 | await github.rest.issues.createComment({ 129 | owner: context.repo.owner, 130 | repo: context.repo.repo, 131 | issue_number: issue.number, 132 | body: `The vendor for key with AAGUID '${aaguid}' is now valid. This issue is being closed automatically.`, 133 | }); 134 | console.log(`Closed issue for key with AAGUID: ${aaguid}`); 135 | } 136 | } 137 | } 138 | } 139 | 140 | - name: Display Merge Log 141 | shell: bash 142 | run: | 143 | echo "Extracting newest 3 entries from merge_log.md" 144 | 145 | # Extract the first 3 '# Merge Log -' sections 146 | awk '/^# Merge Log -/{n++; if(n>3) exit} {print}' merge_log.md > newest_merge_log_entries.txt 147 | 148 | # Adjust header levels from '#' to '###' to fit within the summary 149 | sed 's/^# /### /' newest_merge_log_entries.txt > temp_merge_log_entries.txt 150 | 151 | # Append to GitHub Action Summary without code block 152 | echo "## Merge Log - Newest 3 Entries" >> $GITHUB_STEP_SUMMARY 153 | cat temp_merge_log_entries.txt >> $GITHUB_STEP_SUMMARY 154 | echo "" >> $GITHUB_STEP_SUMMARY 155 | 156 | # Clean up temporary files 157 | rm temp_merge_log_entries.txt newest_merge_log_entries.txt 158 | 159 | - name: Display Detailed Log 160 | shell: bash 161 | run: | 162 | echo "Extracting the 3 newest entries from detailed_log.txt" 163 | 164 | # Extract the first 3 'Detailed Log -' sections 165 | awk '/^Detailed Log -/{n++; if(n>3) exit} {print}' detailed_log.txt > newest_detailed_log_entries.txt 166 | 167 | # Append to GitHub Action Summary 168 | echo "## Detailed Log - Newest 3 Entries" >> $GITHUB_STEP_SUMMARY 169 | echo '```' >> $GITHUB_STEP_SUMMARY 170 | cat newest_detailed_log_entries.txt >> $GITHUB_STEP_SUMMARY 171 | echo '```' >> $GITHUB_STEP_SUMMARY 172 | 173 | # Clean up temporary file 174 | rm newest_detailed_log_entries.txt 175 | 176 | - name: Configure Git 177 | run: | 178 | git config --global user.name 'DevClate' 179 | git config --global user.email 'clate@clatent.com' 180 | 181 | - name: Commit changes 182 | run: | 183 | git add Assets/FidoKeys.json merge_log.md detailed_log.txt 184 | git commit -m "Update FidoKeys.json, merge_log.md, and detailed_log.txt" || echo "No changes to commit" 185 | 186 | - name: Push changes 187 | uses: ad-m/github-push-action@v0.6.0 188 | with: 189 | github_token: ${{ secrets.GITHUB_TOKEN }} 190 | branch: development 191 | -------------------------------------------------------------------------------- /Functions/Public/Find-FIDOKey.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Find FIDO keys eligible for attestation with Entra ID. 4 | 5 | .DESCRIPTION 6 | This function retrieves FIDO keys from a JSON file and filters them based on the provided criteria. 7 | The function supports filtering by Brand, Type (Bio, USB, NFC, BLE), AAGUID, TypeFilterMode, DetailedProperties, and AllProperties. 8 | The results can be displayed in a table, list format, or pipe. 9 | 10 | .PARAMETER Brand 11 | Filter the FIDO keys by Brand. The available brands are listed in the ValidateSet. 12 | 13 | .PARAMETER Type 14 | Filter the FIDO keys by Type. The available types are Bio, USB, NFC, and BLE. 15 | 16 | .PARAMETER AAGUID 17 | Filter the FIDO keys by AAGUID. The AAGUIDs can be provided as an array of strings. 18 | 19 | .PARAMETER AAGUIDFile 20 | Filter the FIDO keys by AAGUIDs imported from a file. Supported file formats are .txt, .csv, and .xlsx. 21 | 22 | .PARAMETER View 23 | Specify the view format for the results. The available options are Table and List 24 | 25 | .PARAMETER TypeFilterMode 26 | Specify the type filter mode. The available options are AtLeastTwo, AtLeastOne, AtLeastThree, and All. 27 | 28 | .PARAMETER AllProperties 29 | Include all properties of the FIDO keys in the output as Json. 30 | 31 | .PARAMETER DetailedProperties 32 | Include detailed properties of the FIDO keys in the output. 33 | 34 | .EXAMPLE 35 | Find-FIDOKey -Brand "Yubico" -Type "USB" -View "Table" 36 | Find FIDO keys from the Yubico brand that support USB and display the results in a table format. 37 | 38 | .EXAMPLE 39 | Find-FIDOKey -AAGUID "12345678" -View "List" 40 | Find FIDO keys with the specified AAGUID and display the results in a list format. 41 | 42 | .EXAMPLE 43 | Find-FIDOKey -AAGUIDFile "AAGUIDs.txt" -View "Table" 44 | Find FIDO keys with AAGUIDs imported from a text file and display the results in a table format. 45 | 46 | .EXAMPLE 47 | Find-FIDOKey -DetailedProperties | Select-Object Vendor, Description, @{Name="ProtocolFamily";Expression={$_.metadataStatement.protocolFamily}} | fl 48 | Find FIDO keys and show the standard properties with version from FIDO Alliance metadata. 49 | #> 50 | function Find-FIDOKey { 51 | [CmdletBinding()] 52 | param ( 53 | 54 | [Parameter()] 55 | [ValidateSet("ACS", "Allthenticator", "Arculus", "AuthenTrend", "Atos", "authenton1", "Chunghwa Telecom", 56 | "Crayonic", "Cryptnox", "Egomet", "Ensurity", "eWBM", "Excelsecu", "Feitian", "FIDO KeyPass", "FT-JCOS", 57 | "Google", "GoTrust", "HID Global", "Hideez", "Hypersecu", "HYPR", "IDCore", "IDEMIA", "IDmelon", "Thales", 58 | "ImproveID", "KEY-ID", "KeyXentic", "KONAI", "NEOWAVE", "NXP Semiconductors", "Nymi", "OCTATCO", "OneSpan", 59 | "OnlyKey", "OpenSK", "Pone Biometrics", "Precision", "RSA", "SafeNet", "Yubico", "Sentry Enterprises", 60 | "SmartDisplayer", "SoloKeys", "Swissbit", "Taglio", "Token Ring", "TOKEN2", "Identiv", "VALMIDO", "Kensington", 61 | "VinCSS", "WiSECURE")] 62 | [string[]]$Brand, 63 | 64 | [Parameter()] 65 | [ValidateSet("Bio", "USB", "NFC", "BLE")] 66 | [string[]]$Type, 67 | 68 | [Parameter( 69 | Position = 0, 70 | ValueFromPipeline = $true 71 | )] 72 | [string[]]$AAGUID, 73 | 74 | [Parameter()] 75 | [string[]]$AAGUIDFile, 76 | 77 | [Parameter()] 78 | [ValidateSet("Table", "List")] 79 | [string]$View, 80 | 81 | [Parameter()] 82 | [ValidateSet("AtLeastTwo", "AtLeastOne", "AtLeastThree", "All")] 83 | [string]$TypeFilterMode = "AtLeastOne", 84 | 85 | [parameter()] 86 | [switch]$AllProperties, 87 | 88 | [Parameter()] 89 | [switch]$DetailedProperties 90 | ) 91 | 92 | # Begin block for initialization 93 | Begin { 94 | # Initialize an array to collect all AAGUIDs 95 | $allAAGUIDs = @() 96 | } 97 | 98 | # Process block to handle pipeline input 99 | Process { 100 | # Collect the pipeline input AAGUIDs 101 | if ($AAGUID) { 102 | $allAAGUIDs += $AAGUID 103 | } 104 | } 105 | 106 | # End block for processing and output 107 | End { 108 | # Include AAGUIDs provided via the -AAGUID parameter 109 | if ($PSBoundParameters.ContainsKey('AAGUID')) { 110 | $allAAGUIDs += $PSBoundParameters['AAGUID'] 111 | } 112 | 113 | # If AAGUIDFile is provided, import the AAGUIDs from the file 114 | if ($AAGUIDFile) { 115 | if (-Not (Test-Path -Path $AAGUIDFile)) { 116 | Write-Error "The AAGUID file was not found at path: $AAGUIDFile" 117 | return 118 | } 119 | else { 120 | $extension = [IO.Path]::GetExtension($AAGUIDFile).ToLowerInvariant() 121 | switch ($extension) { 122 | '.txt' { 123 | $fileContent = Get-Content -Path $AAGUIDFile 124 | } 125 | '.csv' { 126 | $fileContent = Import-Csv -Path $AAGUIDFile | Select-Object -ExpandProperty AAGUID 127 | } 128 | '.xlsx' { 129 | # Check if ImportExcel module is installed 130 | if (-not (Get-Module -ListAvailable -Name ImportExcel)) { 131 | Write-Host "The 'ImportExcel' module is required to import .xlsx files." 132 | Write-Host "Please install it by running 'Install-PSResource -Name ImportExcel -Scope CurrentUser'" 133 | return 134 | } 135 | else { 136 | # Import the module 137 | Import-Module ImportExcel -ErrorAction Stop 138 | # Import data from the .xlsx file 139 | $fileContent = Import-Excel -Path $AAGUIDFile | Select-Object -ExpandProperty AAGUID 140 | } 141 | } 142 | default { 143 | Write-Error "Unsupported file extension: $extension. Supported extensions are .txt, .csv, .xlsx." 144 | return 145 | } 146 | } 147 | 148 | $fileAAGUIDs = $fileContent | Where-Object { -Not [string]::IsNullOrWhiteSpace($_) } | ForEach-Object { $_.Trim() } 149 | 150 | # Combine AAGUIDs from file with any existing AAGUIDs 151 | $allAAGUIDs += $fileAAGUIDs 152 | } 153 | } 154 | 155 | # Remove duplicate AAGUIDs if any 156 | $allAAGUIDs = $allAAGUIDs | Select-Object -Unique 157 | 158 | # Load existing FIDO keys from JSON file 159 | $parentDir = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent 160 | $jsonFilePath = Join-Path -Path $parentDir -ChildPath "Assets/FidoKeys.json" 161 | 162 | if (-Not (Test-Path -Path $jsonFilePath)) { 163 | Write-Error "The JSON file was not found at path: $jsonFilePath" 164 | return 165 | } 166 | 167 | $data = Get-Content -Raw $jsonFilePath | ConvertFrom-Json 168 | $metadata = $data.metadata 169 | $results = $data.keys 170 | 171 | # Filter by AAGUID if provided 172 | if ($allAAGUIDs) { 173 | $results = $results | Where-Object { 174 | $allAAGUIDs -contains $_.AAGUID 175 | } 176 | } 177 | 178 | # Filter by Brand if provided 179 | if ($Brand) { 180 | $results = $results | Where-Object { 181 | $Brand -contains $_.Vendor -or ($Brand | ForEach-Object { $_ -in $_.Description }) 182 | } 183 | } 184 | 185 | # Filter by Type if provided 186 | if ($Type) { 187 | $results = $results | Where-Object { 188 | $typeCount = 0 189 | if ($Type -contains "Bio" -and $_.Bio -eq "Yes") { $typeCount++ } 190 | if ($Type -contains "USB" -and $_.USB -eq "Yes") { $typeCount++ } 191 | if ($Type -contains "NFC" -and $_.NFC -eq "Yes") { $typeCount++ } 192 | if ($Type -contains "BLE" -and $_.BLE -eq "Yes") { $typeCount++ } 193 | 194 | switch ($TypeFilterMode) { 195 | "AtLeastTwo" { $typeCount -ge 2 } 196 | "AtLeastThree" { $typeCount -ge 3 } 197 | "All" { $typeCount -eq $Type.Count } 198 | default { $typeCount -ge 1 } 199 | } 200 | } 201 | } 202 | 203 | # Sort the results by Vendor in alphabetical order 204 | $results = $results | Sort-Object -Property Vendor 205 | 206 | if ($View) { 207 | if ($results.Count -gt 0) { 208 | Write-Host "FIDO Devices eligible for attestation with Entra ID: $($results.Count)" 209 | Write-Host "Database Last Updated: $($metadata.databaseLastUpdated)" 210 | if ($View -eq "Table") { 211 | if ($DetailedProperties -or $FormattedProperties) { 212 | # Directly output the results 213 | $results | Format-Table -AutoSize 214 | } elseif ($AllProperties) { 215 | $results | ConvertTo-Json -Depth 10 | Out-String | Write-Host 216 | } else { 217 | $results | Format-Table -Property Vendor, Description, AAGUID, Bio, USB, NFC, BLE, Version, ValidVendor -AutoSize 218 | } 219 | } else { 220 | if ($DetailedProperties) { 221 | # Directly output the results 222 | $results | Format-List 223 | } elseif ($AllProperties) { 224 | $results | ConvertTo-Json -Depth 10 | Out-String | Write-Host 225 | } else { 226 | $results | Select-Object Vendor, Description, AAGUID, Bio, USB, NFC, BLE, Version, ValidVendor 227 | } 228 | } 229 | } else { 230 | Write-Host "No devices found matching the criteria." 231 | } 232 | } else { 233 | if ($DetailedProperties) { 234 | # Return the results directly 235 | return $results 236 | } elseif ($AllProperties) { 237 | return $results | ConvertTo-Json -Depth 10 238 | } else { 239 | return $results | Select-Object Vendor, Description, AAGUID, Bio, USB, NFC, BLE, Version, ValidVendor 240 | } 241 | } 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /Functions/Private/Merge-FidoData.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Merges FIDO data from a JSON file and a URL source, updating and validating entries. 4 | 5 | .DESCRIPTION 6 | 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. 7 | 8 | .PARAMETER Url 9 | The URL to fetch the FIDO key data from. Defaults to "https://learn.microsoft.com/en-us/entra/identity/authentication/concept-fido2-hardware-vendor". 10 | 11 | .PARAMETER JsonFilePath 12 | The file path to the JSON file containing the original FIDO key data. If not provided, a default path is constructed. 13 | 14 | .PARAMETER LogFilePath 15 | The file path to the log file where changes are documented. If not provided, a default path is constructed. 16 | 17 | .PARAMETER MarkdownFilePath 18 | The file path to the markdown file where changes are documented. If not provided, a default path is constructed. 19 | 20 | .EXAMPLE 21 | Merge-FidoData -JsonFilePath "C:\path\to\FidoKeys.json" -LogFilePath "C:\path\to\merge_log.txt" -MarkdownFilePath "C:\path\to\merge_log.md" 22 | 23 | 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. 24 | 25 | .NOTES 26 | - The function validates vendors against a predefined list and prompts for valid vendor names if necessary. 27 | - Duplicate entries in both JSON and URL data are logged. 28 | - The function ensures that AAGUIDs are unique in the merged data. 29 | - Changes are logged and saved to both log and markdown files. 30 | #> 31 | function Merge-FidoData { 32 | [CmdletBinding()] 33 | param ( 34 | [Parameter()] 35 | [string]$Url = "https://learn.microsoft.com/en-us/entra/identity/authentication/concept-fido2-hardware-vendor", 36 | 37 | [Parameter()] 38 | [string]$JsonFilePath, 39 | 40 | [Parameter()] 41 | [string]$LogFilePath, 42 | 43 | [Parameter()] 44 | [string]$MarkdownFilePath 45 | ) 46 | 47 | # If JsonFilePath is not provided, construct the default path 48 | if (-not $PSBoundParameters.ContainsKey('JsonFilePath')) { 49 | $parentDir = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent 50 | $JsonFilePath = Join-Path -Path $parentDir -ChildPath "Assets/FidoKeys.json" 51 | } 52 | 53 | # If LogFilePath is not provided, construct the default path 54 | if (-not $PSBoundParameters.ContainsKey('LogFilePath')) { 55 | $parentDir = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent 56 | $LogFilePath = Join-Path -Path $parentDir -ChildPath "merge_log.txt" 57 | } 58 | 59 | # If MarkdownFilePath is not provided, construct the default path 60 | if (-not $PSBoundParameters.ContainsKey('MarkdownFilePath')) { 61 | $parentDir = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent 62 | $MarkdownFilePath = Join-Path -Path $parentDir -ChildPath "merge_log.md" 63 | } 64 | 65 | # Read the original JSON file 66 | if (-Not (Test-Path -Path $JsonFilePath)) { 67 | Write-Error "The JSON file was not found at path: $JsonFilePath" 68 | return 69 | } 70 | $jsonData = Get-Content -Raw -Path $JsonFilePath | ConvertFrom-Json 71 | 72 | # Initialize a new JSON structure based on the template 73 | $mergedData = @{ 74 | metadata = @{ 75 | databaseLastUpdated = $jsonData.metadata.databaseLastUpdated 76 | databaseLastChecked = (Get-Date).ToString("yyyy-MM-ddTHH:mm:ssZ") 77 | } 78 | keys = @() 79 | } 80 | 81 | # Create a hash table for quick lookup of JSON data by AAGUID 82 | $jsonDataByAAGUID = @{} 83 | $jsonDuplicates = @{} 84 | foreach ($jsonItem in $jsonData.keys) { 85 | if ($jsonDataByAAGUID.ContainsKey($jsonItem.AAGUID)) { 86 | $jsonDuplicates[$jsonItem.AAGUID] = $jsonItem 87 | } else { 88 | $jsonDataByAAGUID[$jsonItem.AAGUID] = $jsonItem 89 | } 90 | } 91 | 92 | # Create a hash table for quick lookup of URL data by AAGUID 93 | $urlData = Export-EntraFido -Url $Url 94 | $urlDataByAAGUID = @{} 95 | $urlDuplicates = @{} 96 | foreach ($urlItem in $urlData) { 97 | if ($urlDataByAAGUID.ContainsKey($urlItem.AAGUID)) { 98 | $urlDuplicates[$urlItem.AAGUID] = $urlItem 99 | } else { 100 | $urlDataByAAGUID[$urlItem.AAGUID] = $urlItem 101 | } 102 | } 103 | 104 | # Initialize log content with current date and time 105 | $logDate = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' 106 | $logContent = @("Log Date: $logDate") 107 | $markdownContent = @("# Merge Log - $logDate`n") 108 | 109 | # Log duplicates in JSON data 110 | foreach ($duplicate in $jsonDuplicates.Keys) { 111 | $logEntry = "Duplicate entry found in JSON data for AAGUID $duplicate with description '$($jsonDuplicates[$duplicate].Description)'" 112 | $logContent += $logEntry 113 | $markdownContent += "$logEntry`n" 114 | } 115 | 116 | # Log duplicates in URL data 117 | foreach ($duplicate in $urlDuplicates.Keys) { 118 | $logEntry = "Duplicate entry found in URL data for AAGUID $duplicate with description '$($urlDuplicates[$duplicate].Description)'" 119 | $logContent += $logEntry 120 | $markdownContent += "$logEntry`n" 121 | } 122 | 123 | # Function to check if AAGUID exists in the merged data 124 | function Test-AAGUIDExists { 125 | param ( 126 | [string]$aaguid, 127 | [array]$keys 128 | ) 129 | foreach ($key in $keys) { 130 | if ($key.AAGUID -eq $aaguid) { 131 | return $true 132 | } 133 | } 134 | return $false 135 | } 136 | 137 | # Validated set of vendors 138 | $validatedVendors = @( 139 | "ACS", "Allthenticator", "Arculus", "AuthenTrend", "Atos", "authenton1", "Chunghwa Telecom", 140 | "Crayonic", "Cryptnox", "Egomet", "Ensurity", "eWBM", "Excelsecu", "Feitian", "FIDO KeyPass", "FT-JCOS", 141 | "Google", "GoTrust", "HID Global", "Hideez", "Hypersecu", "HYPR", "IDCore", "IDEMIA", "IDmelon", "Thales", 142 | "ImproveID", "KEY-ID", "KeyXentic", "KONAI", "NEOWAVE", "NXP Semiconductors", "Nymi", "OCTATCO", "OneSpan", 143 | "OnlyKey", "OpenSK", "Pone Biometrics", "Precision", "RSA", "SafeNet", "Yubico", "Sentry Enterprises", 144 | "SmartDisplayer", "SoloKeys", "Swissbit", "Taglio", "Token Ring", "TOKEN2", "Identiv", "VALMIDO", "Kensington", 145 | "VinCSS", "WiSECURE" 146 | ) 147 | 148 | # Function to validate a vendor 149 | function Test-ValidVendor { 150 | param ( 151 | [string]$vendor, 152 | [string]$description 153 | ) 154 | $attempts = 0 155 | while ($true) { 156 | if ($validatedVendors -contains $vendor) { 157 | return $vendor 158 | } elseif ($vendor -eq "SKIP") { 159 | if ($attempts -ge 3) { 160 | Write-Warning "Skipping vendor validation for '$description' after 3 attempts" 161 | return $vendor 162 | } else { 163 | Write-Warning "Vendor is currently 'SKIP'. Please enter a valid vendor." 164 | } 165 | } else { 166 | Write-Warning "Unknown vendor detected: $vendor" 167 | } 168 | $vendor = Read-Host "Enter a valid vendor name for '$description' or type 'SKIP' to bypass validation" 169 | if ($vendor -eq "") { 170 | return $vendor 171 | } 172 | $attempts++ 173 | } 174 | } 175 | 176 | $changesMade = $false 177 | 178 | # Loop through the URL data and merge with JSON data 179 | foreach ($urlItem in $urlData) { 180 | $aaguid = $urlItem.AAGUID 181 | $description = $urlItem.Description 182 | if ($jsonDataByAAGUID.ContainsKey($aaguid)) { 183 | # Update the entry in the new JSON with the URL value 184 | $jsonItem = $jsonDataByAAGUID[$aaguid] 185 | foreach ($field in $urlItem.PSObject.Properties.Name) { 186 | if ($jsonItem.$field -ne $urlItem.$field) { 187 | $logEntry = "Updated $field for AAGUID $aaguid with description '$description' from '$($jsonItem.$field)' to '$($urlItem.$field)'" 188 | $logContent += $logEntry 189 | $markdownContent += "$logEntry`n" 190 | $jsonItem.$field = $urlItem.$field 191 | $changesMade = $true 192 | } 193 | } 194 | # Validate the vendor 195 | $originalVendor = $jsonItem.Vendor 196 | $jsonItem.Vendor = Test-ValidVendor -vendor $jsonItem.Vendor -description $description 197 | if ($jsonItem.Vendor -ne $originalVendor) { 198 | $logEntry = "Updated vendor for AAGUID $($jsonItem.AAGUID) with description '$description' from '$originalVendor' to '$($jsonItem.Vendor)'" 199 | $logContent += $logEntry 200 | $markdownContent += "$logEntry`n" 201 | $changesMade = $true 202 | } 203 | if (-not (Test-AAGUIDExists -aaguid $jsonItem.AAGUID -keys $mergedData.keys)) { 204 | $mergedData.keys += [PSCustomObject]@{ 205 | Vendor = $jsonItem.Vendor 206 | Description = $jsonItem.Description 207 | AAGUID = $jsonItem.AAGUID 208 | Bio = $jsonItem.Bio 209 | USB = $jsonItem.USB 210 | NFC = $jsonItem.NFC 211 | BLE = $jsonItem.BLE 212 | } 213 | } 214 | } else { 215 | # Prompt for vendor if not available or invalid 216 | $vendor = Read-Host "Enter vendor for new AAGUID $aaguid with description '$description'" 217 | $vendor = Test-ValidVendor -vendor $vendor -description $description 218 | $urlItem | Add-Member -MemberType NoteProperty -Name Vendor -Value $vendor 219 | if (-not (Test-AAGUIDExists -aaguid $urlItem.AAGUID -keys $mergedData.keys)) { 220 | $mergedData.keys += [PSCustomObject]@{ 221 | Vendor = $urlItem.Vendor 222 | Description = $urlItem.Description 223 | AAGUID = $urlItem.AAGUID 224 | Bio = $urlItem.Bio 225 | USB = $urlItem.USB 226 | NFC = $urlItem.NFC 227 | BLE = $urlItem.BLE 228 | } 229 | $changesMade = $true 230 | } 231 | $logEntry = "Added new AAGUID $aaguid with description '$description' and vendor $vendor" 232 | $logContent += $logEntry 233 | $markdownContent += "$logEntry`n" 234 | } 235 | } 236 | 237 | # Check for AAGUIDs in JSON data but not in URL data and remove them 238 | foreach ($jsonItem in $jsonData.keys) { 239 | if (-not $urlDataByAAGUID.ContainsKey($jsonItem.AAGUID)) { 240 | $logEntry = "Removed AAGUID $($jsonItem.AAGUID) with description '$($jsonItem.Description)' from JSON data" 241 | $logContent += $logEntry 242 | $markdownContent += "$logEntry`n" 243 | $changesMade = $true 244 | } else { 245 | $originalVendor = $jsonItem.Vendor 246 | $jsonItem.Vendor = Test-ValidVendor -vendor $jsonItem.Vendor -description $jsonItem.Description 247 | if ($jsonItem.Vendor -ne $originalVendor) { 248 | $logEntry = "Updated vendor for AAGUID $($jsonItem.AAGUID) with description '$($jsonItem.Description)' from '$originalVendor' to '$($jsonItem.Vendor)'" 249 | $logContent += $logEntry 250 | $markdownContent += "$logEntry`n" 251 | $changesMade = $true 252 | } 253 | if (-not (Test-AAGUIDExists -aaguid $jsonItem.AAGUID -keys $mergedData.keys)) { 254 | $mergedData.keys += [PSCustomObject]@{ 255 | Vendor = $jsonItem.Vendor 256 | Description = $jsonItem.Description 257 | AAGUID = $jsonItem.AAGUID 258 | Bio = $jsonItem.Bio 259 | USB = $jsonItem.USB 260 | NFC = $jsonItem.NFC 261 | BLE = $jsonItem.BLE 262 | } 263 | } 264 | } 265 | } 266 | 267 | # Update the databaseLastUpdated field if changes were made 268 | if ($changesMade) { 269 | $mergedData.metadata.databaseLastUpdated = (Get-Date).ToString("yyyy-MM-ddTHH:mm:ssZ") 270 | } 271 | 272 | # Save the new JSON structure back to the original JSON file 273 | $mergedData | ConvertTo-Json -Depth 10 | Set-Content -Path $JsonFilePath 274 | 275 | # Compare current markdown content with the last one 276 | $lastMarkdownContent = if (Test-Path -Path $MarkdownFilePath) { Get-Content -Raw -Path $MarkdownFilePath } else { "" } 277 | $currentMarkdownContent = $markdownContent -join "`n" 278 | 279 | # Save the markdown content to the markdown file if there are changes and it's different from the last one 280 | if ($changesMade -and $currentMarkdownContent -ne $lastMarkdownContent) { 281 | if (Test-Path -Path $MarkdownFilePath) { 282 | $markdownContent | Out-File -FilePath $MarkdownFilePath -Append 283 | } else { 284 | $markdownContent | Out-File -FilePath $MarkdownFilePath 285 | } 286 | Write-Host "Markdown log saved to $MarkdownFilePath" 287 | } else { 288 | $logContent += "No changes" 289 | Write-Host "No changes detected, Markdown log not updated" 290 | } 291 | 292 | # Always save the log content to the log file 293 | $logContent | Out-File -FilePath $LogFilePath -Append 294 | Write-Host "Log file saved to $LogFilePath" 295 | 296 | Write-Host "Merged data saved to $JsonFilePath" 297 | } -------------------------------------------------------------------------------- /Scripts/Merge-GHFIDOMetaData.ps1: -------------------------------------------------------------------------------- 1 | # Load existing FIDO keys from local JSON file 2 | $existingKeyFile = "Assets/FidoKeys.json" 3 | $existingFIDOKeys = Get-Content $existingKeyFile -Raw | ConvertFrom-Json -Depth 10 4 | 5 | # Fetch FIDO Alliance keys from the MDS3 endpoint 6 | $FIDOUri = "https://mds3.fidoalliance.org/" 7 | $FIDOAURL = Invoke-WebRequest -Uri $FIDOUri 8 | $FIDOAKeys = $FIDOAURL | Get-JWTDetails 9 | 10 | # Initialize the log buffer as an ArrayList 11 | $LogBuffer = New-Object System.Collections.ArrayList 12 | 13 | # Define the log file path 14 | $LogFilePath = "FAmerge_log.txt" 15 | 16 | # Initialize the changes tracking variable 17 | $ChangesMade = $false 18 | 19 | # Create a list of common AAGUIDs between existing keys and FIDO Alliance keys 20 | $CommonAAGUIDs = @($existingFIDOKeys.keys.AAGUID) | Where-Object { 21 | $FIDOAKeys.entries.aaguid -contains $_ 22 | } 23 | 24 | # Function to get a timestamp for logging 25 | function Get-Timestamp { 26 | return (Get-Date -AsUTC).ToString("yyyy-MM-dd HH:mm:ss") 27 | } 28 | 29 | # Function to add log entries to the log buffer 30 | function Add-ToLogBuffer { 31 | param ( 32 | [Parameter(Mandatory = $true)] 33 | [string]$Value 34 | ) 35 | $script:LogBuffer.Add($Value) | Out-Null 36 | } 37 | 38 | # Function to normalize 'versions' property 39 | function Normalize-Versions { 40 | param ([PSCustomObject]$metadataStatement) 41 | 42 | if ($metadataStatement.authenticatorGetInfo) { 43 | $versions = $metadataStatement.authenticatorGetInfo.versions 44 | if ($versions) { 45 | $normalizedVersions = $versions | ForEach-Object { 46 | $_ -replace '([0-9])_([0-9])', '$1.$2' -replace '_', ' ' 47 | } 48 | $metadataStatement.authenticatorGetInfo.versions = $normalizedVersions 49 | } 50 | } 51 | return $metadataStatement 52 | } 53 | 54 | # Function to ensure that specified properties are always arrays 55 | function Ensure-ArrayProperty { 56 | param ( 57 | [Parameter(Mandatory = $true)] 58 | [psobject]$Object, 59 | [Parameter(Mandatory = $true)] 60 | [string[]]$PropertyNames 61 | ) 62 | 63 | foreach ($property in $Object.PSObject.Properties) { 64 | $value = $property.Value 65 | if ($PropertyNames -contains $property.Name) { 66 | if ($value -isnot [System.Collections.IEnumerable] -or $value -is [string]) { 67 | # Wrap the value in an array 68 | $Object.$($property.Name) = @($value) 69 | } 70 | } elseif ($value -is [PSCustomObject]) { 71 | Ensure-ArrayProperty -Object $value -PropertyNames $PropertyNames 72 | } elseif ($value -is [System.Collections.IEnumerable] -and -not ($value -is [string])) { 73 | foreach ($item in $value) { 74 | if ($item -is [PSCustomObject]) { 75 | Ensure-ArrayProperty -Object $item -PropertyNames $PropertyNames 76 | } 77 | } 78 | } 79 | } 80 | } 81 | 82 | # Function to compare two objects and update differences 83 | function Compare-Objects { 84 | param ( 85 | [ref]$obj1, 86 | [ref]$obj2, 87 | $path = '', 88 | [switch]$update, 89 | $AAGUID 90 | ) 91 | 92 | # Exclude automatic properties 93 | $excludedProperties = @('PSPath', 'PSParentPath', 'PSChildName', 'PSDrive', 'PSProvider', 'ReadCount', 'Length', 'Count') 94 | # Get properties that are part of the original JSON data 95 | $jsonProperties = $obj1.Value.PSObject.Properties | Where-Object { 96 | $_.MemberType -eq 'NoteProperty' -and 97 | ($excludedProperties -notcontains $_.Name) 98 | } 99 | 100 | foreach ($prop in $jsonProperties) { 101 | $name = $prop.Name 102 | $value1 = $prop.Value 103 | $value2 = $obj2.Value.PSObject.Properties[$name]?.Value 104 | $currentPath = if ($path) { "$path.$name" } else { $name } 105 | 106 | if ($value1 -is [PSCustomObject] -or $value1 -is [Array]) { 107 | if ($value1 -is [Array] -and $value2 -is [Array]) { 108 | if ($value1.Count -ne $value2.Count) { 109 | if ($update) { 110 | $timestamp = Get-Timestamp 111 | $value1Str = $value1 | ConvertTo-Json -Depth 100 112 | $value2Str = $value2 | ConvertTo-Json -Depth 100 113 | $logMessage = @" 114 | $timestamp - AAGUID: $AAGUID 115 | Path: $currentPath 116 | Old Value: 117 | $value1Str 118 | 119 | New Value: 120 | $value2Str 121 | 122 | "@ 123 | Write-Output $logMessage 124 | Add-ToLogBuffer -Value $logMessage 125 | # Set the changes made flag to true 126 | $script:ChangesMade = $true 127 | # Update the value 128 | if ($prop.IsSettable) { 129 | $obj1.Value.$name = $value2 130 | } 131 | } 132 | } else { 133 | for ($i = 0; $i -lt $value1.Count; $i++) { 134 | $item1 = $value1[$i] 135 | $item2 = $value2[$i] 136 | Compare-Objects -obj1 ([ref]$item1) -obj2 ([ref]$item2) -path "$currentPath`[$i`]" -update:$update -AAGUID $AAGUID 137 | } 138 | } 139 | } else { 140 | Compare-Objects -obj1 ([ref]$value1) -obj2 ([ref]$value2) -path $currentPath -update:$update -AAGUID $AAGUID 141 | } 142 | } else { 143 | if ($value1 -ne $value2) { 144 | if ($update) { 145 | $timestamp = Get-Timestamp 146 | $value1Str = try { $value1 | ConvertTo-Json -Depth 100 } catch { $value1.ToString() } 147 | $value2Str = try { $value2 | ConvertTo-Json -Depth 100 } catch { $value2.ToString() } 148 | $logMessage = "$timestamp - Updating AAGUID $AAGUID : $currentPath : $value1Str -> $value2Str" 149 | Write-Output $logMessage 150 | Add-ToLogBuffer -Value $logMessage 151 | # Set the changes made flag to true 152 | $script:ChangesMade = $true 153 | # Update the value 154 | if ($prop.IsSettable) { 155 | $obj1.Value.$name = $value2 156 | } 157 | } 158 | } 159 | } 160 | } 161 | } 162 | 163 | # Main comparison and update loop 164 | foreach ($AAGUID in $CommonAAGUIDs) { 165 | # Get the entries with the current AAGUID 166 | $EXXXKey = $existingFIDOKeys.keys | Where-Object { $_.AAGUID -eq $AAGUID } 167 | $FAAAKey = $FIDOAKeys.entries | Where-Object { $_.aaguid -eq $AAGUID } 168 | 169 | if ($EXXXKey -and $FAAAKey) { 170 | # Check if timeOfLastStatusChange property exists, and add it if it doesn't 171 | $hasProperty = $EXXXKey.PSObject.Properties.Name -contains "timeOfLastStatusChange" 172 | 173 | # Update timeOfLastStatusChange if different 174 | if ($hasProperty -and $EXXXKey.timeOfLastStatusChange -ne $FAAAKey.timeOfLastStatusChange) { 175 | $timestamp = Get-Timestamp 176 | $logMessage = @" 177 | $timestamp - AAGUID: $AAGUID 178 | Updating timeOfLastStatusChange 179 | Old Value: 180 | $($EXXXKey.timeOfLastStatusChange) 181 | 182 | New Value: 183 | $($FAAAKey.timeOfLastStatusChange) 184 | 185 | "@ 186 | Write-Output $logMessage 187 | Add-ToLogBuffer -Value $logMessage 188 | $EXXXKey.timeOfLastStatusChange = $FAAAKey.timeOfLastStatusChange 189 | # Set the changes made flag to true 190 | $script:ChangesMade = $true 191 | } 192 | # If property doesn't exist but FAAA has it, add it 193 | elseif (-not $hasProperty -and $FAAAKey.timeOfLastStatusChange) { 194 | $timestamp = Get-Timestamp 195 | $logMessage = @" 196 | $timestamp - AAGUID: $AAGUID 197 | Adding timeOfLastStatusChange property 198 | New Value: 199 | $($FAAAKey.timeOfLastStatusChange) 200 | 201 | "@ 202 | Write-Output $logMessage 203 | Add-ToLogBuffer -Value $logMessage 204 | 205 | # Add the property to the object 206 | $EXXXKey | Add-Member -MemberType NoteProperty -Name "timeOfLastStatusChange" -Value $FAAAKey.timeOfLastStatusChange 207 | 208 | # Set the changes made flag to true 209 | $script:ChangesMade = $true 210 | } 211 | 212 | # Normalize versions if metadataStatement exists 213 | if ($EXXXKey.PSObject.Properties.Name -contains "metadataStatement" -and $EXXXKey.metadataStatement) { 214 | $EXXXKey.metadataStatement = Normalize-Versions $EXXXKey.metadataStatement 215 | } 216 | 217 | if ($FAAAKey.PSObject.Properties.Name -contains "metadataStatement" -and $FAAAKey.metadataStatement) { 218 | $FAAAKey.metadataStatement = Normalize-Versions $FAAAKey.metadataStatement 219 | } 220 | 221 | # Check if EXXX entry has no metadataStatement but FAAA does 222 | $hasMetadataStatement = $EXXXKey.PSObject.Properties.Name -contains "metadataStatement" 223 | if ((-not $hasMetadataStatement -or $EXXXKey.metadataStatement -eq $null) -and $FAAAKey.metadataStatement) { 224 | $timestamp = Get-Timestamp 225 | $logMessage = @" 226 | $timestamp - AAGUID: $AAGUID 227 | Adding metadataStatement from FAAA to EXXX entry 228 | Vendor: $($EXXXKey.Vendor) 229 | Description: $($EXXXKey.Description) 230 | 231 | "@ 232 | Write-Output $logMessage 233 | Add-ToLogBuffer -Value $logMessage 234 | 235 | # Add or set metadataStatement property 236 | if (-not $hasMetadataStatement) { 237 | $EXXXKey | Add-Member -MemberType NoteProperty -Name "metadataStatement" -Value $FAAAKey.metadataStatement 238 | } else { 239 | $EXXXKey.metadataStatement = $FAAAKey.metadataStatement 240 | } 241 | 242 | # Set the changes made flag to true 243 | $script:ChangesMade = $true 244 | } 245 | 246 | # Remove null properties that aren't needed 247 | if ($EXXXKey.PSObject.Properties.Name -contains "authenticatorGetInfo" -and $EXXXKey.authenticatorGetInfo -eq $null) { 248 | $EXXXKey.PSObject.Properties.Remove("authenticatorGetInfo") 249 | $script:ChangesMade = $true 250 | } 251 | 252 | if ($EXXXKey.PSObject.Properties.Name -contains "statusReports" -and $EXXXKey.statusReports -eq $null) { 253 | # If we have status reports in FAAA, copy them over 254 | if ($FAAAKey.statusReports) { 255 | $EXXXKey.statusReports = $FAAAKey.statusReports 256 | } else { 257 | # Otherwise, remove the null property 258 | $EXXXKey.PSObject.Properties.Remove("statusReports") 259 | } 260 | $script:ChangesMade = $true 261 | } 262 | 263 | # If timeOfLastStatusChange is null but exists in FAAA, copy it over 264 | if ($EXXXKey.PSObject.Properties.Name -contains "timeOfLastStatusChange" -and 265 | $EXXXKey.timeOfLastStatusChange -eq $null -and 266 | $FAAAKey.timeOfLastStatusChange) { 267 | $EXXXKey.timeOfLastStatusChange = $FAAAKey.timeOfLastStatusChange 268 | $script:ChangesMade = $true 269 | } 270 | 271 | # Compare and update metadataStatement if both exist 272 | if ($EXXXKey.PSObject.Properties.Name -contains "metadataStatement" -and 273 | $EXXXKey.metadataStatement -and 274 | $FAAAKey.metadataStatement) { 275 | Compare-Objects -obj1 ([ref]$EXXXKey.metadataStatement) -obj2 ([ref]$FAAAKey.metadataStatement) -update -AAGUID $AAGUID 276 | } 277 | 278 | # Compare and update statusReports if both exist 279 | if ($EXXXKey.PSObject.Properties.Name -contains "statusReports" -and 280 | $FAAAKey.PSObject.Properties.Name -contains "statusReports" -and 281 | $EXXXKey.statusReports -and 282 | $FAAAKey.statusReports) { 283 | Compare-Objects -obj1 ([ref]$EXXXKey.statusReports) -obj2 ([ref]$FAAAKey.statusReports) -path 'statusReports' -update -AAGUID $AAGUID 284 | } 285 | } # End of if ($EXXXKey -and $FAAAKey) block 286 | } # End of foreach loop 287 | 288 | # Enforce that 'versions' properties are always arrays 289 | foreach ($key in $existingFIDOKeys.keys) { 290 | if ($key.metadataStatement) { 291 | Ensure-ArrayProperty -Object $key.metadataStatement -PropertyNames 'versions' 292 | } 293 | } 294 | 295 | # Check if any changes were made 296 | if (-not $ChangesMade) { 297 | $message = "No changes have been detected since last run." 298 | Write-Output $message 299 | Add-ToLogBuffer -Value $message 300 | } 301 | 302 | # Add the separator line to the log buffer at the top 303 | $separator = "===== Script Run on $(Get-Date -AsUTC -Format 'yyyy-MM-dd HH:mm:ss') UTC =====" 304 | $LogBuffer.InsertRange(0, @('', $separator)) 305 | 306 | # Prepend the collected log entries to the log file 307 | if (Test-Path $LogFilePath) { 308 | $existingContent = Get-Content -Path $LogFilePath -Raw 309 | $LogContent = ($LogBuffer -join "`n") + "`n" + $existingContent 310 | Set-Content -Path $LogFilePath -Value $LogContent 311 | } else { 312 | # If the file doesn't exist, create it with the log content 313 | $LogContent = $LogBuffer -join "`n" 314 | Set-Content -Path $LogFilePath -Value $LogContent 315 | } 316 | 317 | # Save the updated $existingFIDOKeys to the JSON file 318 | $UpdatedKeysJson = $existingFIDOKeys | ConvertTo-Json -Depth 100 319 | Set-Content -Path 'Assets/FidoKeys.json' -Value $UpdatedKeysJson 320 | 321 | # Calculate counts 322 | $existingKeyCount = $existingFIDOKeys.keys.Count 323 | $FIDOAKeyCount = $FIDOAKeys.entries.Count 324 | 325 | # Set outputs for GitHub Actions 326 | $githubOutput = $env:GITHUB_OUTPUT 327 | Add-Content -Path $githubOutput -Value "existingKeyCount=$existingKeyCount" 328 | Add-Content -Path $githubOutput -Value "FIDOAKeyCount=$FIDOAKeyCount" 329 | Add-Content -Path $githubOutput -Value "changesMade=$ChangesMade" 330 | -------------------------------------------------------------------------------- /.vscode/fido2-metadata.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "FIDO2 Metadata Collection", 4 | "type": "object", 5 | "required": ["keys", "metadata"], 6 | "properties": { 7 | "keys": { 8 | "type": "array", 9 | "description": "Collection of FIDO2 authenticator metadata entries", 10 | "items": { 11 | "type": "object", 12 | "properties": { 13 | "Vendor": { 14 | "type": "string" 15 | }, 16 | "Description": { 17 | "type": "string" 18 | }, 19 | "AAGUID": { 20 | "type": "string", 21 | "pattern": "^[0-9a-fA-F-]{36}$" 22 | }, 23 | "Bio": { 24 | "type": "string", 25 | "enum": [ 26 | "✅", 27 | "❌" 28 | ] 29 | }, 30 | "USB": { 31 | "type": "string", 32 | "enum": [ 33 | "✅", 34 | "❌" 35 | ] 36 | }, 37 | "NFC": { 38 | "type": "string", 39 | "enum": [ 40 | "✅", 41 | "❌" 42 | ] 43 | }, 44 | "BLE": { 45 | "type": "string", 46 | "enum": [ 47 | "✅", 48 | "❌" 49 | ] 50 | }, 51 | "Version": { 52 | "type": "string" 53 | }, 54 | "ValidVendor": { 55 | "type": "string", 56 | "enum": [ 57 | "Yes", 58 | "No" 59 | ] 60 | }, 61 | "metadataStatement": { 62 | "type": "object", 63 | "properties": { 64 | "legalHeader": { 65 | "type": "string" 66 | }, 67 | "aaguid": { 68 | "type": "string", 69 | "pattern": "^[0-9a-fA-F-]{36}$" 70 | }, 71 | "description": { 72 | "type": "string" 73 | }, 74 | "alternativeDescriptions": { 75 | "type": "object", 76 | "patternProperties": { 77 | "^[a-z]{2}(?:-[A-Z]{2})?$": { 78 | "type": "string" 79 | } 80 | }, 81 | "additionalProperties": false 82 | }, 83 | "authenticatorVersion": { 84 | "type": "integer" 85 | }, 86 | "protocolFamily": { 87 | "type": "string" 88 | }, 89 | "schema": { 90 | "type": "integer" 91 | }, 92 | "upv": { 93 | "type": "array", 94 | "items": { 95 | "type": "object", 96 | "properties": { 97 | "major": { 98 | "type": "integer" 99 | }, 100 | "minor": { 101 | "type": "integer" 102 | } 103 | } 104 | } 105 | }, 106 | "authenticationAlgorithms": { 107 | "type": "array", 108 | "items": { 109 | "type": "string" 110 | } 111 | }, 112 | "publicKeyAlgAndEncodings": { 113 | "type": "array", 114 | "items": { 115 | "type": "string" 116 | } 117 | }, 118 | "attestationTypes": { 119 | "type": "array", 120 | "items": { 121 | "type": "string" 122 | } 123 | }, 124 | "userVerificationDetails": { 125 | "type": "array", 126 | "items": { 127 | "type": "array", 128 | "items": { 129 | "type": "object", 130 | "properties": { 131 | "userVerificationMethod": { 132 | "type": "string" 133 | }, 134 | "caDesc": { 135 | "type": "object", 136 | "properties": { 137 | "base": { 138 | "type": "integer" 139 | }, 140 | "minLength": { 141 | "type": "integer" 142 | }, 143 | "maxRetries": { 144 | "type": "integer" 145 | }, 146 | "blockSlowdown": { 147 | "type": "integer" 148 | } 149 | } 150 | }, 151 | "baDesc": { 152 | "type": "object", 153 | "properties": { 154 | "selfAttestedFRR": { 155 | "type": "integer" 156 | }, 157 | "selfAttestedFAR": { 158 | "type": "integer" 159 | }, 160 | "iAPARThreshold": { 161 | "type": "integer" 162 | }, 163 | "maxTemplates": { 164 | "type": "integer" 165 | }, 166 | "maxRetries": { 167 | "type": "integer" 168 | }, 169 | "blockSlowdown": { 170 | "type": "integer" 171 | } 172 | } 173 | } 174 | } 175 | } 176 | } 177 | }, 178 | "keyProtection": { 179 | "type": "array", 180 | "items": { 181 | "type": "string" 182 | } 183 | }, 184 | "isKeyRestricted": { 185 | "type": "boolean" 186 | }, 187 | "matcherProtection": { 188 | "type": "array", 189 | "items": { 190 | "type": "string" 191 | } 192 | }, 193 | "cryptoStrength": { 194 | "type": "integer" 195 | }, 196 | "attachmentHint": { 197 | "type": "array", 198 | "items": { 199 | "type": "string" 200 | } 201 | }, 202 | "tcDisplay": { 203 | "type": "array" 204 | }, 205 | "attestationRootCertificates": { 206 | "type": "array", 207 | "items": { 208 | "type": "string" 209 | } 210 | }, 211 | "icon": { 212 | "type": "string" 213 | }, 214 | "authenticatorGetInfo": { 215 | "type": "object", 216 | "properties": { 217 | "versions": { 218 | "type": "array", 219 | "items": { 220 | "type": "string" 221 | } 222 | }, 223 | "extensions": { 224 | "type": "array", 225 | "items": { 226 | "type": "string" 227 | } 228 | }, 229 | "aaguid": { 230 | "type": "string" 231 | }, 232 | "options": { 233 | "type": "object" 234 | }, 235 | "maxMsgSize": { 236 | "type": "integer" 237 | }, 238 | "pinUvAuthProtocols": { 239 | "type": "array", 240 | "items": { 241 | "type": "integer" 242 | } 243 | }, 244 | "maxCredentialCountInList": { 245 | "type": "integer" 246 | }, 247 | "maxCredentialIdLength": { 248 | "type": "integer" 249 | }, 250 | "transports": { 251 | "type": "array", 252 | "items": { 253 | "type": "string" 254 | } 255 | }, 256 | "algorithms": { 257 | "type": "array" 258 | }, 259 | "minPINLength": { 260 | "type": ["integer", "null"] 261 | }, 262 | "firmwareVersion": { 263 | "type": "integer" 264 | }, 265 | "maxCredBlobLength": { 266 | "type": ["integer", "null"] 267 | }, 268 | "maxRPIDsForSetMinPINLength": { 269 | "type": ["integer", "null"] 270 | }, 271 | "forcePINChange": { 272 | "type": "boolean" 273 | } 274 | } 275 | } 276 | } 277 | }, 278 | "statusReports": { 279 | "type": "array", 280 | "items": { 281 | "type": "object", 282 | "properties": { 283 | "status": { 284 | "type": "string" 285 | }, 286 | "effectiveDate": { 287 | "type": "string" 288 | }, 289 | "authenticatorVersion": { 290 | "type": "integer" 291 | }, 292 | "url": { 293 | "type": "string" 294 | }, 295 | "certificationDescriptor": { 296 | "type": "string" 297 | }, 298 | "certificateNumber": { 299 | "type": "string" 300 | }, 301 | "certificationPolicyVersion": { 302 | "type": "string" 303 | }, 304 | "certificationRequirementsVersion": { 305 | "type": "string" 306 | } 307 | } 308 | } 309 | }, 310 | "timeOfLastStatusChange": { 311 | "type": "string" 312 | } 313 | }, 314 | "required": [ 315 | "Vendor", 316 | "Description", 317 | "AAGUID", 318 | "Bio", 319 | "USB", 320 | "NFC", 321 | "BLE", 322 | "Version", 323 | "ValidVendor" 324 | ], 325 | "additionalProperties": false, 326 | "propertyOrder": [ 327 | "Vendor", 328 | "Description", 329 | "AAGUID", 330 | "Bio", 331 | "USB", 332 | "NFC", 333 | "BLE", 334 | "Version", 335 | "ValidVendor", 336 | "metadataStatement", 337 | "statusReports", 338 | "timeOfLastStatusChange" 339 | ] 340 | } 341 | }, 342 | "metadata": { 343 | "type": "object", 344 | "properties": { 345 | "databaseLastChecked": { 346 | "type": "string", 347 | "description": "Date and time when the FIDO database was last checked", 348 | "format": "date-time" 349 | }, 350 | "databaseLastUpdated": { 351 | "type": "string", 352 | "description": "Date and time when the FIDO database was last updated", 353 | "format": "date-time" 354 | } 355 | }, 356 | "required": ["databaseLastChecked", "databaseLastUpdated"], 357 | "additionalProperties": false 358 | } 359 | }, 360 | "additionalProperties": false 361 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | Here you will find all changes per version 4 | 5 | ## v0.0.20 6 | 7 | ### 0.0.20 Working 8 | 9 | **New Keys:** 10 | 11 | - Hyper FIDO Pro NFC - 23195a52-62d9-40fa-8ee5-23b173f4fb52 12 | - Hyper FIDO Pro (CTAP2.1, CTAP2.0, U2F) - 6999180d-630c-442d-b8f7-424b90a43fae 13 | - DEMIA SOLVO Fly 80 R3 FIDO Card c - dda9aa35-aaf1-4d3c-b6db-7902fd7dbbbf 14 | - IDEMIA SOLVO Fly 80 R3 FIDO Card e - def8ab1a-9f91-44f1-a103-088d8dc7d681 15 | 16 | **Updated Keys:** 17 | 18 | - Updated 'NFC' for AAGUID '3f59672f-20aa-4afe-b6f4-7e5e916b6d98' from '✅' to '❌'. 19 | - Updated 'USB' for AAGUID 'b12eac35-586c-4809-a4b1-d81af6c305cf' from '✅' to '❌'. 20 | - Updated 'NFC' for AAGUID 'b12eac35-586c-4809-a4b1-d81af6c305cf' from '✅' to '❌'. 21 | - Updated 'NFC' for AAGUID '9d3df6ba-282f-11ed-a261-0242ac120002' from '✅' to '❌'. 22 | - Updated 'USB' for AAGUID '39a5647e-1853-446c-a1f6-a79bae9f5bc7' from '❌' to '✅'. 23 | - Updated 'BLE' for AAGUID '39a5647e-1853-446c-a1f6-a79bae9f5bc7' from '❌' to '✅'. 24 | - Updated 'USB' for AAGUID '820d89ed-d65a-409e-85cb-f73f0578f82a' from '❌' to '✅'. 25 | - Updated 'BLE' for AAGUID '820d89ed-d65a-409e-85cb-f73f0578f82a' from '❌' to '✅'. 26 | 27 | ## v0.0.19 28 | 29 | 10 New keys have been added to the approved attestation list for Entra. 30 | 31 | **New Keys:** 32 | 33 | - Chipwon Clife Key | 930b0c03-ef46-4ac4-935c-538dccd1fcdb 34 | 35 | - HID Crescendo 4000 FIDO | aa79f476-ea00-417e-9628-1e8365123922 36 | 37 | - ID-One Key | 82b0a720-127a-4788-b56d-d1d4b2d82eac 38 | 39 | - ID-One Key | f2145e86-211e-4931-b874-e22bba7d01cc 40 | 41 | - VeridiumID Passkey Android SDK | 8d4378b0-725d-4432-b3c2-01fcdaf46286 42 | 43 | - VeridiumID Passkey iOS SDK | 1e906e14-77af-46bc-ae9f-fe6ef18257e4 44 | 45 | - VinCSS FIDO2 Fingerprint | 9012593f-43e4-4461-a97a-d92777b55d74 46 | 47 | - YubiKey 5 Series with NFC - Enhanced PIN | 662ef48a-95e2-4aaa-a6c1-5b9c40375824 48 | 49 | - YubiKey 5 Series with NFC - Enhanced PIN (Enterprise Profile) | b2c1a50b-dad8-4dc7-ba4d-0ce9597904bc 50 | 51 | - YubiKey 5 Series with NFC KVZR57 | 9eb7eabc-9db5-49a1-b6c3-555a802093f4 52 | 53 | ## v0.0.18 54 | 55 | - Updated Metada from FIDO Alliance 56 | 57 | The reason for so many removals is that these keys had unapproved models and Microsoft removed them. 58 | 59 | **Updated Keys:** 60 | 61 | - 'USB' and ‘NFC’ for AAGUID '69700f79-d1fb-472e-bd9b-a3a3b9a9eda0' from '❌' to '✅'. 62 | 63 | - 'USB' for AAGUID 'e1a96183-5016-4f24-b55b-e3ae23614cc6' from '❌' to '✅'. 64 | 65 | - 'USB' for AAGUID '61250591-b2bc-4456-b719-0b17be90bb30' from '❌' to '✅'. 66 | 67 | - 'USB' for AAGUID 'd821a7d4-e97c-4cb6-bd82-4237731fd4be' from '❌' to '✅'. 68 | 69 | - 'NFC' for AAGUID '931327dd-c89b-406c-a81e-ed7058ef36c6' from '❌' to '✅'. 70 | 71 | - 'USB' and ‘NFC’ for AAGUID 'b12eac35-586c-4809-a4b1-d81af6c305cf' from '❌' to '✅'. 72 | 73 | - 'NFC' for AAGUID '9d3df6ba-282f-11ed-a261-0242ac120002' from '❌' to '✅'. 74 | 75 | - 'USB' and ‘BLE’ for AAGUID 'd41f5a69-b817-4144-a13c-9ebd6d9254d6' from '❌' to '✅'. 76 | 77 | - 'USB' for AAGUID '95442b2e-f15e-4def-b270-efb106facb4e' from '❌' to '✅'. 78 | 79 | - 'USB' for AAGUID '10c70715-2a9a-4de1-b0aa-3cff6d496d39' from '❌' to '✅'. 80 | 81 | - 'USB' for AAGUID '998f358b-2dd2-4cbe-a43a-e8107438dfb3' from '❌' to '✅'. 82 | 83 | - 'USB' for AAGUID '87dbc5a1-4c94-4dc8-8a47-97d800fd1f3c' from '❌' to '✅'. 84 | 85 | - 'USB' for AAGUID '146e77ef-11eb-4423-b847-ce77864e9411' from '❌' to '✅'. 86 | 87 | - 'NFC' for AAGUID '30b5035e-d297-4ff1-b00b-addc96ba6a98' from '❌' to '✅'. 88 | 89 | - 'USB' for AAGUID '9f77e279-a6e2-4d58-b700-31e5943c6a98' from '❌' to '✅'. 90 | 91 | - 'USB' for AAGUID 'ab32f0c6-2239-afbb-c470-d2ef4e254db7' from '❌' to '✅'. 92 | 93 | - 'NFC' for AAGUID '3f59672f-20aa-4afe-b6f4-7e5e916b6d98' from '❌' to '✅'. 94 | 95 | **Removed Keys:** 96 | 97 | - 'YubiKey 5 FIPS Series (RC Preview)' with AAGUID 'd2fbd093-ee62-488d-9dad-1e36389f8826'. 98 | 99 | - 'GoldKey Security Token' with AAGUID '0db01cd6-5618-455b-bb46-1ec203d3213e'. 100 | 101 | - 'SafeKey/Classic (FP)' with AAGUID 'e41b42a3-60ac-4afb-8757-a98f2d7f6c9f'. 102 | 103 | - 'Veridium Android SDK' with AAGUID '5ea308b2-7ac7-48b9-ac09-7e2da9015f8c'. 104 | 105 | - 'ID-One Key' with AAGUID 'f2145e86-211e-4931-b874-e22bba7d01cc'. 106 | 107 | - 'Ledger Stax FIDO2 Authenticator' with AAGUID '6e24d385-004a-16a0-7bfe-efd963845b34'. 108 | 109 | - 'Android Authenticator with SafetyNet Attestation' with AAGUID 'b93fd961-f2e6-462f-b122-82002247de78'. 110 | 111 | - 'Deepnet SafeKey/Classic (USB)' with AAGUID 'b9f6b7b6-f929-4189-bca9-dd951240c132'. 112 | 113 | - 'ESS Smart Card Inc. Authenticator' with AAGUID '5343502d-5343-5343-6172-644649444f32'. 114 | 115 | - 'IIST FIDO2 Authenticator' with AAGUID '4b89f401-464e-4745-a520-486ddfc5d80e'. 116 | 117 | - 'YubiKey 5 FIPS Series with Lightning Preview' with AAGUID '5b0e46ba-db02-44ac-b979-ca9b84f5e335'. 118 | 119 | - 'eWBM eFA500 FIDO2 Authenticator' with AAGUID '361a3082-0278-4583-a16f-72a527f973e4'. 120 | 121 | - 'YubiKey 5 FIPS Series with NFC Preview' with AAGUID '62e54e98-c209-4df3-b692-de71bb6a8528'. 122 | 123 | - 'Security Key NFC by Yubico - Enterprise Edition Preview' with AAGUID '2772ce93-eb4b-4090-8b73-330f48477d73'. 124 | 125 | - 'Dapple Authenticator from Dapple Security Inc.' with AAGUID '6dae43be-af9c-417b-8b9f-1b611168ec60'. 126 | 127 | - 'Ledger Nano S Plus FIDO2 Authenticator' with AAGUID '58b44d0b-0a7c-f33a-fd48-f7153c871352'. 128 | 129 | - 'WiSECURE Blentity FIDO2 Authenticator' with AAGUID '5753362b-4e6b-6345-7b2f-255438404c75'. 130 | 131 | - 'VeridiumID Passkey Android SDK' with AAGUID '8d4378b0-725d-4432-b3c2-01fcdaf46286'. 132 | 133 | - 'Samsung Pass' with AAGUID '53414d53-554e-4700-0000-000000000000'. 134 | 135 | - 'Ledger Nano X FIDO2 Authenticator' with AAGUID 'fcb1bcb4-f370-078c-6993-bc24d0ae3fbe'. 136 | 137 | - 'Feitian FIDO Smart Card' with AAGUID '2c0df832-92de-4be1-8412-88a8f074df4a'. 138 | 139 | - 'Ledger Flex FIDO2 Authenticator' with AAGUID '1d8cac46-47a1-3386-af50-e88ae46fe802'. 140 | 141 | - 'ZTPass SmartAuth' with AAGUID '99bf4610-ec26-4252-b31f-7380ccd59db5'. 142 | 143 | - 'YubiKey 5 Series with Lightning Preview' with AAGUID '3124e301-f14e-4e38-876d-fbeeb090e7bf'. 144 | 145 | - 'Pone Biometrics OFFPAD Authenticator' with AAGUID '09591fc6-9811-48f7-8f57-b9f23df6413f'. 146 | 147 | - 'WinMagic FIDO Eazy - Phone' with AAGUID 'f56f58b3-d711-4afc-ba7d-6ac05f88cb19'. 148 | 149 | - 'OneKey FIDO2 Bluetooth Authenticator' with AAGUID '70e7c36f-f2f6-9e0d-07a6-bcc243262e6b'. 150 | 151 | - 'OneKey FIDO2 Authenticator' with AAGUID '69e7c36f-f2f6-9e0d-07a6-bcc243262e6b'. 152 | 153 | - 'YubiKey 5 FIPS Series with Lightning (RC Preview)' with AAGUID '9e66c661-e428-452a-a8fb-51f7ed088acf'. 154 | 155 | - 'Feitian ePass FIDO2-NFC Plus Authenticator' with AAGUID '260e3021-482d-442d-838c-7edfbe153b7e'. 156 | 157 | - 'Veridium iOS SDK' with AAGUID '6e8d1eae-8d40-4c25-bcf8-4633959afc71'. 158 | 159 | - 'WinMagic FIDO Eazy - Software' with AAGUID '31c3f7ff-bf15-4327-83ec-9336abcbcd34'. 160 | 161 | - 'USB/NFC Passcode Authenticator' with AAGUID 'cfcb13a2-244f-4b36-9077-82b79d6a7de7'. 162 | 163 | - 'YubiKey 5 Series with NFC Preview' with AAGUID '34f5766d-1536-4a24-9033-0e294e510fb0'. 164 | 165 | - 'ellipticSecure MIRkey USB Authenticator' with AAGUID 'eb3b131e-59dc-536a-d176-cb7306da10f5'. 166 | 167 | - 'WinMagic FIDO Eazy - TPM' with AAGUID '970c8d9c-19d2-46af-aa32-3f448db49e35'. 168 | 169 | - 'eToken Fusion NFC PIV Enterprise' with AAGUID 'c3f47802-de73-4dfc-ba22-671fe3304f90'. 170 | 171 | - 'KeyVault Secp256R1 FIDO2 CTAP2 Authenticator' with AAGUID 'd61d3b87-3e7c-4aea-9c50-441c371903ad'. 172 | 173 | - 'Ledger Nano S FIDO2 Authenticator' with AAGUID '341e4da9-3c2e-8103-5a9f-aad887135200'. 174 | 175 | - 'IDEMIA SOLVO Fly 80 R1 FIDO Card Draft' with AAGUID '3fd410dc-8ab7-4b86-a1cb-c7174620b2dc'. 176 | 177 | - 'Security Key NFC by Yubico Preview' with AAGUID '760eda36-00aa-4d29-855b-4012a182cdeb'. 178 | 179 | - 'OCTATCO EzQuant FIDO2 AUTHENTICATOR' with AAGUID 'bc2fe499-0d8e-4ffe-96f3-94a82840cf8c'. 180 | 181 | - 'ID-One Key' with AAGUID '82b0a720-127a-4788-b56d-d1d4b2d82eac'. 182 | 183 | - 'VivoKey Apex FIDO2' with AAGUID 'd7a423ad-3e19-4492-9200-78137dccc136'. 184 | 185 | - 'VeridiumID Passkey iOS SDK' with AAGUID '1e906e14-77af-46bc-ae9f-fe6ef18257e4'. 186 | 187 | - 'Hideez Key 3 FIDO2' with AAGUID '3e078ffd-4c54-4586-8baa-a77da113aec5'. 188 | 189 | - 'TruU Windows Authenticator' with AAGUID 'ba86dc56-635f-4141-aef6-00227b1b9af6'. 190 | 191 | - 'FIDO Alliance TruU Sample FIDO2 Authenticator' with AAGUID 'ca87cb70-4c1b-4579-a8e8-4efdd7c007e0'. 192 | 193 | - 'YubiKey 5 FIPS Series with NFC (RC Preview)' with AAGUID 'ce6bf97f-9f69-4ba7-9032-97adc6ca5cf1'. 194 | 195 | - 'TruU Windows Authenticator' with AAGUID '95e4d58c-056e-4a65-866d-f5a69659e880'. 196 | 197 | ## v0.0.17 198 | 199 | - Updated Metadata from FIDO Alliance 200 | - Adding JSON schema to easily catch wrong formatting or fields. 201 | - Created a function to fix keys that weren't formatted correctly and ensure they are in the correct order 202 | - Created a Pester test to make sure JSON key file is formatted correctly 203 | 204 | **Database Update** 205 | 206 | New Vendors: 207 | 208 | - Deepnet 209 | - GoldKey 210 | - SafeKey 211 | - SHALO 212 | - T-Shiled 213 | - VeridiumID 214 | 215 | New Keys: 216 | 217 | | AAGUID | Vendor | Description | 218 | | ------------------------------------ | ---------- | ----------------------------------------------------------- | 219 | | c89e6a38-6c00-5426-5aa5-c9cbf48f0382 | ACS | FIDO Authenticator NF | 220 | | b9f6b7b6-f929-4189-bca9-dd951240c132 | Deepnet | SafeKey/Classic (USB) | 221 | | b12eac35-586c-4809-a4b1-d81af6c305cf | Deepnet | SafeKey/Classic (NFC) | 222 | | e41b42a3-60ac-4afb-8757-a98f2d7f6c9f | Deepnet | SafeKey/Classic (FP) | 223 | | 78ba3993-d784-4f44-8d6e-cc0a8ad5230e | Feitian | ePass FIDO-NFC(CTAP2.1, CTAP2.0, U2F) | 224 | | 39589099-9a75-49fc-afaa-801ca211c62a | Feitian | ePass FIDO-NFC (Enterprise Profile) (CTAP2.1, CTAP2.0, U2F) | 225 | | 0db01cd6-5618-455b-bb46-1ec203d3213e | GoldKey | Security Token | 226 | | c4ddaf11-3032-4e77-b3b9-3a340369b9ad | HID | Crescendo Fusion | 227 | | 57235694-51a5-4a4d-a81a-f42185df6502 | SHALO | SHALO AUTH | 228 | | 7787a482-13e8-4784-8a06-c7ed49a7aaf4 | Swissbit | iShield Key 2 | 229 | | e400ef8c-711d-4692-af46-7f2cf7da23ad | Swissbit | iShield Key 2 Enterprise | 230 | | 5eaff75a-dd43-451f-af9f-87c9eeae293e | Swissbit | iShield Key 2 FIPS Enterprise | 231 | | 817cdab8-0d51-4de1-a821-e25b88519cf3 | Swissbit | iShield Key 2 FIPS | 232 | | 882adaf5-3aa9-4708-8e7d-3957103775b4 | T-Shield | TrustSec FIDO2 Bio and client PIN version | 233 | | 8d4378b0-725d-4432-b3c2-01fcdaf46286 | VeridiumID | Passkey Android SDK | 234 | | 1e906e14-77af-46bc-ae9f-fe6ef18257e4 | VeridiumID | Passkey iOS SDK | 235 | 236 | ## v0.0.16 237 | 238 | **Database Update** 239 | 240 | New Vendors: 241 | 242 | - Android 243 | - Dapple Security 244 | - Eviden 245 | - Foongton 246 | - GSTAG 247 | - ID-One 248 | - IIST 249 | - Infineon Technologies AG 250 | - KeyVault 251 | - Ledger 252 | - Nitrokey 253 | - OneKey 254 | - Samsung 255 | - Securité Carte à Puce 256 | - TruU 257 | - Veridium 258 | - VeroCard 259 | - Vivokey 260 | - WinMagic 261 | - ZTPass 262 | 263 | New Keys: 264 | 265 | | AAGUID | Vendor | Description | 266 | | ---------------------------------------- | ------------------------ | -------------------------------------------------------------------- | 267 | | `eb3b131e-59dc-536a-d176-cb7306da10f5` | ellipticSecure | ellipticSecure MIRkey USB Authenticator | 268 | | `8da0e4dc-164b-454e-972e-88f362b23d59` | Eviden | CardOS FIDO2 Token | 269 | | `46544d5d-8f5d-4db4-89ac-ea8977073fff` | Foongton | Foongtone FIDO Authenticator | 270 | | `773c30d9-5919-4e96-a4f5-db65e95cf890` | GSTAG | GSTAG OAK FIDO2 Authenticator | 271 | | `7991798a-a7f3-487f-98c0-3faf7a458a04` | HID Global | HID Crescendo Key V3 | 272 | | `2a55aee6-27cb-42c0-bc6e-04efe999e88a` | HID Global | HID Crescendo 4000 | 273 | | `82b0a720-127a-4788-b56d-d1d4b2d82eac` | ID-One | ID-One Key | 274 | | `f2145e86-211e-4931-b874-e22bba7d01cc` | ID-One | ID-One Key | 275 | | `4b89f401-464e-4745-a520-486ddfc5d80e` | IIST | IIST FIDO2 Authenticator | 276 | | `cfcb13a2-244f-4b36-9077-82b79d6a7de7` | Infineon Technologies AG | USB/NFC Passcode Authenticator | 277 | | `58b44d0b-0a7c-f33a-fd48-f7153c871352` | Ledger | Ledger Nano S Plus FIDO2 Authenticator | 278 | | `fcb1bcb4-f370-078c-6993-bc24d0ae3fbe` | Ledger | Ledger Nano X FIDO2 Authenticator | 279 | | `341e4da9-3c2e-8103-5a9f-aad887135200` | Ledger | Ledger Nano S FIDO2 Authenticator | 280 | | `2cd2f727-f6ca-44da-8f48-5c2e5da000a2` | Nitrokey | Nitrokey 3 AM | 281 | | `70e7c36f-f2f6-9e0d-07a6-bcc243262e6b` | OneKey | OneKey FIDO2 Bluetooth Authenticator | 282 | | `53414d53-554e-4700-0000-000000000000` | Samsung | Samsung Pass | 283 | | `5343502d-5343-5343-6172-644649444f32` | Securité Carte à Puce | ESS Smart Card Inc. Authenticator | 284 | | `050dd0bc-ff20-4265-8d5d-305c4b215192` | Thales | eToken Fusion FIPS | 285 | | `10c70715-2a9a-4de1-b0aa-3cff6d496d39` | Thales | eToken Fusion NFC FIPS | 286 | | `c3f47802-de73-4dfc-ba22-671fe3304f90` | Thales | eToken Fusion NFC PIV Enterprise | 287 | | `146e77ef-11eb-4423-b847-ce77864e9411` | Thales | eToken Fusion NFC PIV | 288 | | `ba86dc56-635f-4141-aef6-00227b1b9af6` | TruU | TruU Windows Authenticator | 289 | | `95e4d58c-056e-4a65-866d-f5a69659e880` | TruU | TruU Windows Authenticator | 290 | | `5ea308b2-7ac7-48b9-ac09-7e2da9015f8c` | Veridium | Veridium Android SDK | 291 | | `6e8d1eae-8d40-4c25-bcf8-4633959afc71` | Veridium | Veridium iOS SDK | 292 | | `99ed6c29-4573-4847-816d-78ad8f1c75ef` | VeroCard | VeroCard FIDO2 Authenticator | 293 | | `d7a423ad-3e19-4492-9200-78137dccc136` | VivoKey | VivoKey Apex FIDO2 | 294 | | `31c3f7ff-bf15-4327-83ec-9336abcbcd34` | Winmagic | WinMagic FIDO Eazy - Software | 295 | | `970c8d9c-19d2-46af-aa32-3f448db49e35` | WinMagic | WinMagic FIDO Eazy - TPM | 296 | | `f56f58b3-d711-4afc-ba7d-6ac05f88cb19` | WinMagic | WinMagic FIDO Eazy - Phone | 297 | | `b7d3f68e-88a6-471e-9ecf-2df26d041ede` | Yubico | Security Key NFC by Yubico | 298 | | `9ff4cc65-6154-4fff-ba09-9e2af7882ad2` | Yubico | Security Key NFC by Yubico - Enterprise Edition (Enterprise Profile) | 299 | | `34f5766d-1536-4a24-9033-0e294e510fb0` | Yubico | YubiKey 5 Series with NFC Preview | 300 | | `6ec5cff2-a0f9-4169-945b-f33b563f7b99` | Yubico | YubiKey Bio Series - Multi-protocol Edition (Enterprise Profile) | 301 | | `8c39ee86-7f9a-4a95-9ba3-f6b097e5c2ee` | Yubico | YubiKey Bio Series - FIDO Edition (Enterprise Profile) | 302 | | `24673149-6c86-42e7-98d9-433fb5b73296` | Yubico | YubiKey 5 Series with Lightning | 303 | | `3a662962-c6d4-4023-bebb-98ae92e78e20` | Yubico | YubiKey 5 FIPS Series with Lightning (Enterprise Profile) | 304 | | `20ac7a17-c814-4833-93fe-539f0d5e3389` | Yubico | YubiKey 5 Series (Enterprise Profile) | 305 | | `b90e7dc1-316e-4fee-a25a-56a666a670fe` | Yubico | YubiKey 5 Series with Lightning (Enterprise Profile) | 306 | | `760eda36-00aa-4d29-855b-4012a182cdeb` | Yubico | Security Key NFC by Yubico Preview | 307 | | `fcc0118f-cd45-435b-8da1-9782b2da0715` | Yubico | YubiKey 5 FIPS Series with NFC | 308 | | `ff4dac45-ede8-4ec2-aced-cf66103f4335` | Yubico | YubiKey 5 Series | 309 | | `7b96457d-e3cd-432b-9ceb-c9fdd7ef7432` | Yubico | YubiKey 5 FIPS Series with Lightning | 310 | | `97e6a830-c952-4740-95fc-7c78dc97ce47` | Yubico | YubiKey Bio Series - Multi-protocol Edition (Enterprise Profile) | 311 | | `6ab56fad-881f-4a43-acb2-0be065924522` | Yubico | YubiKey 5 Series with NFC (Enterprise Profile) | 312 | | `d2fbd093-ee62-488d-9dad-1e36389f8826` | Yubico | YubiKey 5 FIPS Series (RC Preview) | 313 | | `4599062e-6926-4fe7-9566-9e8fb1aedaa0` | Yubico | YubiKey 5 Series (Enterprise Profile) | 314 | | `d7781e5d-e353-46aa-afe2-3ca49f13332a` | Yubico | YubiKey 5 Series with NFC | 315 | | `62e54e98-c209-4df3-b692-de71bb6a8528` | Yubico | YubiKey 5 FIPS Series with NFC Preview | 316 | | `34744913-4f57-4e6e-a527-e9ec3c4b94e6` | Yubico | YubiKey Bio Series - Multi-protocol Edition | 317 | | `ed042a3a-4b22-4455-bb69-a267b652ae7e` | Yubico | Security Key NFC by Yubico - Enterprise Edition | 318 | | `3b24bf49-1d45-4484-a917-13175df0867b` | Yubico | YubiKey 5 Series with Lightning (Enterprise Profile) | 319 | | `3124e301-f14e-4e38-876d-fbeeb090e7bf` | Yubico | YubiKey 5 Series with Lightning Preview | 320 | | `9e66c661-e428-452a-a8fb-51f7ed088acf` | Yubico | YubiKey 5 FIPS Series with Lightning (RC Preview) | 321 | | `ce6bf97f-9f69-4ba7-9032-97adc6ca5cf1` | Yubico | YubiKey 5 FIPS Series with NFC (RC Preview) | 322 | | `2772ce93-eb4b-4090-8b73-330f48477d73` | Yubico | Security Key NFC by Yubico - Enterprise Edition Preview | 323 | | `ad08c78a-4e41-49b9-86a2-ac15b06899e2` | Yubico | YubiKey Bio Series - FIDO Edition | 324 | | `905b4cb4-ed6f-4da9-92fc-45e0d4e9b5c7` | Yubico | YubiKey 5 FIPS Series (Enterprise Profile) | 325 | | `b415094c-49d3-4c8b-b3fe-7d0ad28a6bc4` | ZTPass | ZTPass SmartAuth | 326 | 327 | ## v0.0.15 328 | 329 | **Database Update** 330 | 331 | * 516d3969-5a57-5651-5958-4e7a49434167 - SmartDisplayer BobeePass FIDO2 Authenticator - Bio is now supported 332 | 333 | ## v0.0.14 334 | 335 | **Improvements** 336 | 337 | * Find-FIDOKey -DetailedProperties gives you access to all of the JSON properties, so you can select your own output 338 | * When importing an Excel file if you don’t have ImportExcel imported, it will warn you 339 | * GitHub Action to automatically pull FIDO Alliance info and merge it with the JSON data 340 | * Web Version: If you click on a key it will show more of the info, and there is also a button to show the full JSON 341 | 342 | **Changes** 343 | 344 | * Find-FIDOkey -AllProperties now shows a nice configured JSON file of the key(s) you want to see. You can use -DetailedProperties if you want to create your own custom view 345 | 346 | ## v0.0.13 347 | 348 | **Enhancements** 349 | 350 | * Filter by FIDO version from FIDO Alliance (PowerShell and Web Version) 351 | * Using ValidateSet for versions ("FIDO U2F", "FIDO 2.0", "FIDO 2.1", "FIDO 2.1 PRE") 352 | * Added -AllProperties 353 | * Default to terminal shows basic fields, but added -AllProperties that I’ll add more of the useful fields first 354 | * Show-FIDODbVersion now shows you your current version and if it needs to be updated 355 | 356 | ## v0.0.11 357 | 358 | **Changes** 359 | 360 | * Database Updated (see Merge Logs) 361 | 362 | ## v0.0.10 363 | 364 | **Changes** 365 | 366 | * Removed Requirement for PSParseHTML as it is not needed as of yet 367 | 368 | ## v0.0.9 369 | 370 | **Enhancements** 371 | 372 | * Fixed Log Output 373 | * Created Get-FIDODbLog 374 | * You can view the master database log from terminal 375 | * FidoKeys.json automatically copies to web version on update 376 | * Database Updated - See Database Log File 377 | 378 | ## v0.0.8 379 | 380 | **Enhancements** 381 | 382 | - See what version of database you have and see what the newest version is (No longer need to use -NewestVersion parameter, it does it automatically) 383 | - Show last time Database was checked for newest updates 384 | - Created a merge log that when there is a change it will show the changes that occurred 385 | 386 | ## v0.0.7 387 | 388 | **Enhancements** 389 | 390 | - Ability to see your current database version and what is the newest version out 391 | 392 | ## v0.0.6 393 | 394 | **Enhancements** 395 | 396 | - Added database last updated field 397 | 398 | **Fixed** 399 | 400 | - JSON path for Keys now working in ./Assets/FidoKeys.json 401 | -------------------------------------------------------------------------------- /Scripts/Merge-GHFidoData.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Merges FIDO key data from a URL with existing JSON data and logs any changes. 4 | 5 | .DESCRIPTION 6 | Fetches FIDO key data from the specified URL and merges it with local JSON data. 7 | Handles updates, additions, and removals of keys, validates vendors, and logs changes. 8 | Updates metadata and environment variables for GitHub Actions. 9 | 10 | .PARAMETER Url 11 | The URL to fetch FIDO key data from. 12 | Defaults to Microsoft's hardware vendor page. 13 | 14 | .PARAMETER JsonFilePath 15 | The path to the local JSON file containing existing FIDO key data. 16 | Default is 'Assets/FidoKeys.json'. 17 | 18 | .PARAMETER MarkdownFilePath 19 | The path to the markdown file for logging merge results. 20 | Default is 'merge_log.md'. 21 | 22 | .PARAMETER DetailedLogFilePath 23 | The path to the detailed log file. 24 | Default is 'detailed_log.txt'. 25 | 26 | .PARAMETER ValidVendorsFilePath 27 | The path to the JSON file containing valid vendors. 28 | Default is 'Assets/valid_vendors.json'. 29 | 30 | .EXAMPLE 31 | Merge-GHFidoData 32 | 33 | .NOTES 34 | Author: Clayton Tyger 35 | Date: 12-01-2024 36 | #> 37 | 38 | Function Merge-GHFidoData { 39 | [CmdletBinding()] 40 | param ( 41 | [Parameter()] 42 | [string]$Url = "https://learn.microsoft.com/en-us/entra/identity/authentication/concept-fido2-hardware-vendor", 43 | [Parameter()] 44 | [string]$JsonFilePath = "Assets/FidoKeys.json", 45 | [Parameter()] 46 | [string]$MarkdownFilePath = "merge_log.md", 47 | [Parameter()] 48 | [string]$DetailedLogFilePath = "detailed_log.txt", 49 | [Parameter()] 50 | [string]$ValidVendorsFilePath = "Assets/valid_vendors.json" 51 | ) 52 | 53 | $ErrorActionPreference = 'Stop' 54 | 55 | # Load existing JSON data 56 | try { 57 | if (-not (Test-Path -Path $JsonFilePath)) { 58 | $jsonData = @{ 59 | metadata = @{ 60 | databaseLastChecked = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') 61 | databaseLastUpdated = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') 62 | } 63 | keys = @() 64 | } 65 | } 66 | else { 67 | $jsonData = Get-Content -Raw -Path $JsonFilePath | ConvertFrom-Json 68 | } 69 | } 70 | catch { 71 | Write-Error "Failed to load JSON data: $_" 72 | return 73 | } 74 | 75 | # Load valid vendors data 76 | try { 77 | $ValidVendors = (Get-Content -Raw -Path $ValidVendorsFilePath | ConvertFrom-Json).vendors 78 | } 79 | catch { 80 | Write-Error "Failed to load valid vendors data: $_" 81 | return 82 | } 83 | 84 | # Initialize variables 85 | $changesDetected = [ref]$false 86 | $updateDatabaseLastUpdated = $false 87 | $changesAreSame = $false 88 | $keysNowValid = New-Object System.Collections.ArrayList 89 | $issueEntries = New-Object System.Collections.ArrayList 90 | $loggedInvalidVendors = New-Object System.Collections.ArrayList 91 | $currentLogEntries = New-Object System.Collections.ArrayList 92 | 93 | # Import the Test-GHValidVendor function 94 | . "$PSScriptRoot\Test-GHValidVendor.ps1" 95 | 96 | # Initialize merged data 97 | $mergedData = @{ 98 | metadata = @{ 99 | databaseLastChecked = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') 100 | databaseLastUpdated = $jsonData.metadata.databaseLastUpdated 101 | } 102 | keys = @() 103 | } 104 | 105 | # Initialize an empty hashtable 106 | $jsonDataByAAGUID = @{} 107 | 108 | # Populate the hashtable with AAGUIDs as keys and single items as values 109 | foreach ($key in $jsonData.keys) { 110 | $jsonDataByAAGUID[$key.AAGUID] = $key 111 | } 112 | 113 | # Fetch data from URL 114 | try { 115 | $urlData = Export-GHEntraFido -Url $Url 116 | } 117 | catch { 118 | Write-Error "Failed to fetch data from URL: $_" 119 | return 120 | } 121 | 122 | # Index URL data by AAGUID 123 | $urlDataByAAGUID = $urlData | Group-Object -AsHashTable -Property AAGUID 124 | 125 | # Prepare for logging 126 | $logDate = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' 127 | 128 | # Parse existing log content 129 | $existingMarkdownContent = if (Test-Path -Path $MarkdownFilePath) { 130 | Get-Content -Raw -Path $MarkdownFilePath 131 | } 132 | else { 133 | "" 134 | } 135 | 136 | $existingDetailedLogContent = if (Test-Path -Path $DetailedLogFilePath) { 137 | Get-Content -Raw -Path $DetailedLogFilePath 138 | } 139 | else { 140 | "" 141 | } 142 | 143 | # Initialize content 144 | $markdownContent = New-Object System.Collections.ArrayList 145 | $detailedLogContent = New-Object System.Collections.ArrayList 146 | $detailedLogContent.Add("Detailed Log - $logDate") # Initialize with the log date 147 | $envFilePath = "$PSScriptRoot/env_vars.txt" 148 | 149 | # Parse existing markdown content to extract last log entries 150 | if ($existingMarkdownContent -ne "") { 151 | # Regex to match each log entry 152 | $pattern = "(?ms)^# Merge Log - \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\s*(.*?)(?=^# Merge Log - \d{4}-\d{2}-\d{2}|\z)" 153 | $matches = [regex]::Matches($existingMarkdownContent, $pattern) 154 | if ($matches.Count -gt 0) { 155 | # The first match is the most recent log entries 156 | $lastLogEntriesSection = $matches[0].Groups[1].Value 157 | $existingLogEntries = $lastLogEntriesSection -split "`r?`n" | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne "" } 158 | } 159 | } 160 | 161 | # Initialize existingLogEntries as an array even if it's null or empty 162 | $existingLogEntries = @($existingLogEntries) 163 | 164 | # Collect changes in a separate variable 165 | $detailedChanges = @() 166 | 167 | # Merge data and handle vendors 168 | foreach ($aaguid in $urlDataByAAGUID.Keys) { 169 | $urlItem = $urlDataByAAGUID[$aaguid] 170 | $description = $urlItem.Description 171 | $vendor = "" 172 | 173 | if ($jsonDataByAAGUID.ContainsKey($aaguid)) { 174 | # Existing entry 175 | $existingItem = $jsonDataByAAGUID[$aaguid] 176 | $vendor = $existingItem.Vendor 177 | $vendorRef = [ref]$vendor 178 | 179 | # Create a hashtable with parameters 180 | $ValidVendorParams = @{ 181 | vendor = $vendorRef 182 | description = $description 183 | aaguid = $aaguid 184 | ValidVendors = $ValidVendors 185 | markdownContent = $markdownContent 186 | detailedLogContent = $detailedLogContent 187 | loggedInvalidVendors = $loggedInvalidVendors 188 | issueEntries = $issueEntries 189 | existingLogEntries = $existingLogEntries 190 | changesDetected = $changesDetected 191 | IsNewEntry = $false 192 | currentLogEntries = $currentLogEntries 193 | } 194 | 195 | # Call the function with splatting 196 | $validVendor = Test-GHValidVendor @ValidVendorParams 197 | $vendor = $vendorRef.Value 198 | 199 | # Access $existingItem.Version 200 | $existingVersion = $existingItem.Version 201 | 202 | # Get the latest version from metadataStatement.authenticatorGetInfo.versions 203 | $latestVersion = $null 204 | if ($existingItem.metadataStatement?.authenticatorGetInfo?.versions) { 205 | $latestVersion = $existingItem.metadataStatement.authenticatorGetInfo.versions[-1] 206 | } 207 | 208 | # Compare and update the Version property if needed 209 | if ($latestVersion -and $existingVersion -ne $latestVersion) { 210 | # Update the Version property 211 | $existingItem.Version = $latestVersion 212 | $changesDetected.Value = $true 213 | $updateDatabaseLastUpdated = $true 214 | 215 | # Log the change 216 | $logEntry = "Updated 'Version' for AAGUID '$aaguid' from '$existingVersion' to '$latestVersion'." 217 | $currentLogEntries.Add($logEntry) 218 | $detailedChanges += $logEntry # Collect changes separately 219 | } 220 | 221 | # Check for changes in specific properties 222 | $propertiesToCheck = @('Description', 'Bio', 'USB', 'NFC', 'BLE') 223 | foreach ($property in $propertiesToCheck) { 224 | $existingValue = $existingItem.$property 225 | $newValue = $urlItem.$property 226 | if ($existingValue -ne $newValue) { 227 | $existingItem.$property = $newValue 228 | $changesDetected.Value = $true 229 | $updateDatabaseLastUpdated = $true 230 | $logEntry = "Updated '$property' for AAGUID '$aaguid' from '$existingValue' to '$newValue'." 231 | $currentLogEntries.Add($logEntry) 232 | $detailedChanges += $logEntry # Collect changes separately 233 | } 234 | } 235 | 236 | # Normalize ValidVendor values to strings 237 | $existingValidVendor = [string]$existingItem.ValidVendor 238 | $newValidVendor = [string]$validVendor 239 | 240 | # Update ValidVendor status if changed 241 | if ($existingValidVendor -ne $newValidVendor) { 242 | $existingItem.ValidVendor = $newValidVendor 243 | $changesDetected.Value = $true 244 | $updateDatabaseLastUpdated = $true 245 | 246 | if ($newValidVendor -eq 'Yes') { 247 | # Check if the valid vendor name is in the vendor value 248 | $matchedValidVendor = $ValidVendors | Where-Object { $vendor -match $_ } 249 | 250 | # If no valid vendor name is found in the current vendor value 251 | if ($null -eq $matchedValidVendor) { 252 | # Find the valid vendor that matches the description or is closest to the current vendor name 253 | $bestMatch = $ValidVendors | Where-Object { $description -match $_ } | Select-Object -First 1 254 | 255 | if ($null -ne $bestMatch) { 256 | $oldVendor = $vendor 257 | $vendor = $bestMatch 258 | $existingItem.Vendor = $vendor 259 | $logEntry = "Updated vendor name for AAGUID '$aaguid' from '$oldVendor' to '$vendor' based on validated vendor list." 260 | $currentLogEntries.Add($logEntry) 261 | $detailedChanges += $logEntry 262 | } 263 | } 264 | 265 | $keysNowValid.Add($aaguid) 266 | $logEntry = "Vendor '$vendor' for description '$description' has become valid." 267 | $currentLogEntries.Add($logEntry) 268 | $detailedChanges += $logEntry 269 | # Add logic to close the corresponding GitHub issue if it exists 270 | $issueTitle = "Invalid Vendor Detected for AAGUID $aaguid : $vendor" 271 | $existingIssue = $issueEntries | Where-Object { $_ -match [regex]::Escape($issueTitle) } 272 | if ($existingIssue) { 273 | $issueEntries.Add("$issueTitle|CLOSE") 274 | } 275 | } 276 | elseif ($newValidVendor -eq 'No') { 277 | $logEntry = "Vendor '$vendor' for description '$description' has become invalid." 278 | $currentLogEntries.Add($logEntry) 279 | $detailedChanges += $logEntry 280 | # Create an issue when a vendor becomes invalid 281 | $issueTitle = "Vendor Became Invalid for AAGUID $aaguid : $vendor" 282 | $issueBody = $logEntry 283 | 284 | # Check if the issue already exists 285 | $existingIssue = $issueEntries | Where-Object { $_ -match [regex]::Escape($issueTitle) } 286 | if (-not $existingIssue) { 287 | $issueEntries.Add("$issueTitle|$issueBody|InvalidVendor") 288 | } 289 | } 290 | } 291 | 292 | # Add updated item to merged data 293 | $mergedData.keys += $existingItem 294 | } 295 | else { 296 | # New entry 297 | $vendorRef = [ref]$vendor 298 | $ValidVendorParams = @{ 299 | vendor = $vendorRef 300 | description = $description 301 | aaguid = $aaguid 302 | ValidVendors = $ValidVendors 303 | markdownContent = $markdownContent 304 | detailedLogContent = $detailedLogContent 305 | loggedInvalidVendors = $loggedInvalidVendors 306 | issueEntries = $issueEntries 307 | existingLogEntries = $existingLogEntries 308 | changesDetected = $changesDetected 309 | IsNewEntry = $true 310 | currentLogEntries = $currentLogEntries 311 | } 312 | 313 | # Call the function with splatting 314 | $validVendor = Test-GHValidVendor @ValidVendorParams 315 | $vendor = $vendorRef.Value 316 | 317 | # If vendor is valid, ensure it matches a valid vendor name from the list 318 | if ($validVendor -eq 'Yes') { 319 | # Now check if vendor is still empty or doesn't match a valid vendor name 320 | if ([string]::IsNullOrWhiteSpace($vendor) -or -not ($ValidVendors -contains $vendor)) { 321 | # Find the valid vendor that matches the description 322 | $bestMatch = $ValidVendors | Where-Object { $description -match $_ } | Select-Object -First 1 323 | 324 | if ($null -ne $bestMatch) { 325 | $oldVendor = $vendor 326 | $vendor = $bestMatch 327 | $logEntry = "Updated vendor name for new AAGUID '$aaguid' from '$oldVendor' to '$vendor' based on validated vendor list." 328 | $currentLogEntries.Add($logEntry) 329 | $detailedChanges += $logEntry 330 | } 331 | # If still no match, try to find any valid vendor in our list to use 332 | elseif ([string]::IsNullOrWhiteSpace($vendor)) { 333 | # As a last resort, use the first valid vendor that appears in the description 334 | foreach ($validVendorName in $ValidVendors) { 335 | if ($description -match $validVendorName) { 336 | $vendor = $validVendorName 337 | $logEntry = "Set vendor name for AAGUID '$aaguid' to '$vendor' based on description match." 338 | $currentLogEntries.Add($logEntry) 339 | $detailedChanges += $logEntry 340 | break 341 | } 342 | } 343 | 344 | # If still no vendor found, use the first word of the description 345 | if ([string]::IsNullOrWhiteSpace($vendor)) { 346 | $firstWord = ($description -split ' ')[0] 347 | if (-not [string]::IsNullOrWhiteSpace($firstWord)) { 348 | $vendor = $firstWord 349 | $logEntry = "Set vendor name for AAGUID '$aaguid' to '$vendor' based on first word of description." 350 | $currentLogEntries.Add($logEntry) 351 | $detailedChanges += $logEntry 352 | } 353 | } 354 | } 355 | } 356 | } 357 | # If vendor is still empty but validVendor is 'No', set to "Unknown" 358 | elseif ([string]::IsNullOrWhiteSpace($vendor)) { 359 | $vendor = "Unknown" 360 | $logEntry = "Set vendor name for invalid AAGUID '$aaguid' to 'Unknown'." 361 | $currentLogEntries.Add($logEntry) 362 | $detailedChanges += $logEntry 363 | } 364 | 365 | $newItem = [pscustomobject]@{ 366 | Vendor = $vendor 367 | Description = $description 368 | AAGUID = $aaguid 369 | Bio = $urlItem.Bio 370 | USB = $urlItem.USB 371 | NFC = $urlItem.NFC 372 | BLE = $urlItem.BLE 373 | Version = $urlItem.Version 374 | ValidVendor = $validVendor # This is now guaranteed to be just "Yes" or "No" 375 | authenticatorGetInfo = $urlItem.authenticatorGetInfo 376 | statusReports = $urlItem.statusReports 377 | timeOfLastStatusChange = $urlItem.timeOfLastStatusChange 378 | } 379 | $mergedData.keys += $newItem 380 | $changesDetected.Value = $true 381 | $updateDatabaseLastUpdated = $true 382 | 383 | # Log new entry if vendor is valid 384 | if ($validVendor -eq 'Yes') { 385 | $logEntry = "Added new entry for AAGUID '$aaguid' with description '$description' and vendor '$vendor'." 386 | $currentLogEntries.Add($logEntry) 387 | $detailedChanges += $logEntry # Collect changes separately 388 | } 389 | # Note: Invalid vendor logging for new entries is handled inside Test-GHValidVendor 390 | } 391 | } 392 | 393 | # Handle removed entries 394 | foreach ($aaguid in $jsonDataByAAGUID.Keys) { 395 | if (-not $urlDataByAAGUID.ContainsKey($aaguid)) { 396 | $removedItem = $jsonDataByAAGUID[$aaguid] 397 | $logEntry = "Entry removed for description '$($removedItem.Description)' with AAGUID '$aaguid'." 398 | $currentLogEntries.Add($logEntry) 399 | $detailedChanges += $logEntry # Collect changes separately 400 | $changesDetected.Value = $true 401 | $updateDatabaseLastUpdated = $true 402 | } 403 | } 404 | 405 | # Update metadata 406 | if ($updateDatabaseLastUpdated) { 407 | $mergedData.metadata.databaseLastUpdated = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') 408 | } 409 | $mergedData.metadata.databaseLastChecked = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') 410 | 411 | # Sort and write the merged data 412 | $mergedData.keys = $mergedData.keys | Sort-Object Vendor 413 | $jsonOutput = $mergedData | ConvertTo-Json -Depth 10 414 | Set-Content -Path $JsonFilePath -Value $jsonOutput -Encoding utf8 415 | 416 | # Compare current log entries with last run's log entries 417 | if ($changesDetected.Value) { 418 | # Ensure both arrays are initialized 419 | $normalizedExistingLogEntries = @($existingLogEntries | ForEach-Object { $_.Trim() }) 420 | $normalizedCurrentLogEntries = @($currentLogEntries | ForEach-Object { $_.Trim() }) 421 | 422 | $differences = Compare-Object -ReferenceObject $normalizedExistingLogEntries -DifferenceObject $normalizedCurrentLogEntries | Where-Object { $_.SideIndicator -ne '==' } 423 | 424 | if ($differences.Count -eq 0) { 425 | $changesAreSame = $true 426 | } 427 | } 428 | 429 | # Adjust logging based on whether changes are the same as last run 430 | if ($changesDetected.Value -and -not $changesAreSame) { 431 | # Only update merge_log.md when there are new changes different from last run 432 | Write-Host "New changes detected. Updating merge_log.md." 433 | # Update merge_log.md 434 | $newMergeContent = "# Merge Log - $logDate`n`n" + 435 | ($currentLogEntries -join "`n`n") + 436 | "`n`n`n" + 437 | $existingMarkdownContent.Trim() 438 | Set-Content -Path $MarkdownFilePath -Value $newMergeContent -Encoding utf8 439 | # Update detailed_log.txt 440 | # Clear the existing content 441 | $detailedLogContent.Clear() 442 | $detailedLogContent.Add("DETAILED LOG - $logDate") 443 | $detailedLogContent.Add("") 444 | # Ensure collected changes are added to $detailedLogContent 445 | for ($i = 0; $i -lt $detailedChanges.Count; $i++) { 446 | $detailedLogContent.Add($detailedChanges[$i]) 447 | if ($i -lt ($detailedChanges.Count - 1)) { 448 | $detailedLogContent.Add("") # Add an empty line between entries 449 | } 450 | } 451 | $detailedLogContent = $detailedLogContent | ForEach-Object { $_.TrimEnd("`n", "`r") } 452 | # Add extra newline between entries 453 | $newDetailedContent = ($detailedLogContent -join "`n") + "`n`n`n`n" + $existingDetailedLogContent.TrimStart("`n", "`r") 454 | Set-Content -Path $DetailedLogFilePath -Value $newDetailedContent -Encoding utf8 455 | } 456 | else { 457 | # Do not update merge_log.md 458 | if (-not $changesDetected.Value) { 459 | Write-Host "No changes detected. Not updating merge_log.md." 460 | } 461 | elseif ($changesAreSame) { 462 | Write-Host "Changes are the same as the last run. Not updating merge_log.md." 463 | } 464 | # Update detailed_log.txt with "No changes detected during this run." 465 | $detailedLogContent.Clear() 466 | $detailedLogContent.Add("DETAILED LOG - $logDate") 467 | $detailedLogContent.Add("") 468 | $detailedLogContent.Add("No changes detected during this run.") 469 | # Add consistent spacing between entries 470 | $newDetailedContent = ($detailedLogContent -join "`n") + "`n`n`n`n" + $existingDetailedLogContent.TrimStart("`n", "`r") 471 | Set-Content -Path $DetailedLogFilePath -Value $newDetailedContent -Encoding utf8 472 | } 473 | 474 | # Update environment variables for GitHub Actions 475 | if ($issueEntries -and $issueEntries.Count -gt 0) { 476 | $issueEntriesString = $issueEntries -join "`n" 477 | # Escape special characters for GitHub Actions 478 | if ($null -ne $issueEntriesString -and $issueEntriesString -ne "") { 479 | $issueEntriesEscaped = $issueEntriesString.Replace('%', '%25').Replace("`r", '%0D').Replace("`n", '%0A').Replace("'", '%27').Replace('"', '%22') 480 | "ISSUE_ENTRIES=$issueEntriesEscaped" | Out-File -FilePath $envFilePath -Encoding utf8 -Append 481 | } 482 | } 483 | 484 | # Write keys now valid to the environment variables file 485 | if ($keysNowValid -and $keysNowValid.Count -gt 0) { 486 | $keysNowValidString = $keysNowValid | Select-Object -Unique | Sort-Object 487 | $keysNowValidString = $keysNowValidString -join "`n" 488 | # Escape special characters for GitHub Actions 489 | if ($null -ne $keysNowValidString -and $keysNowValidString -ne "") { 490 | $keysNowValidEscaped = $keysNowValidString.Replace('%', '%25').Replace("`r", '%0D').Replace("`n", '%0A').Replace("'", '%27').Replace('"', '%22') 491 | "KEYS_NOW_VALID=$keysNowValidEscaped" | Out-File -FilePath $envFilePath -Encoding utf8 -Append 492 | } 493 | } 494 | } 495 | Merge-GHFidoData -------------------------------------------------------------------------------- /Explorer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 || Vendor | 224 |Description | 225 |AAGUID | 226 |Bio | 227 |USB | 228 |NFC | 229 |BLE | 230 |Version | 231 |
|---|