scripts/ArgumentCompleters.ps1
<# .SYNOPSIS Provides intelligent tab completion for Zero Trust Assessment test IDs and names. .DESCRIPTION This script implements sophisticated argument completion for the -Tests parameter of Invoke-ZtAssessment and Invoke-ZeroTrustAssessment cmdlets. It enables users to search and select tests using partial test IDs, test titles, categories, or common abbreviations. Key Features: - Fast tab completion with caching for improved performance - Smart search with synonym support (e.g., 'mfa' finds multifactor authentication tests) - Multi-word search capability - Support for comma-separated test lists - Tooltip display with test details - Intelligent sorting (exact matches first, then relevance-based) .EXAMPLE Basic test ID completion: Invoke-ZtAssessment -Tests 217<TAB> Completes to specific test ID starting with 217 (e.g., 21773, 21793) .EXAMPLE Partial test ID with multiple matches: Invoke-ZtAssessment -Tests 21<CTRL+SPACE> Shows all test IDs starting with 21 (e.g., 210, 211, 212, etc.) .EXAMPLE Search by test title keywords: Invoke-ZtAssessment -Tests mfa<TAB> Shows all multifactor authentication related tests .EXAMPLE Multi-word search: Invoke-ZtAssessment -Tests "mfa high"<TAB> Shows tests with both "mfa" and "high" in title/risk level .EXAMPLE Synonym support: Invoke-ZtAssessment -Tests ca<TAB> .NOTES File: ArgumentCompleters.ps1 Author: Zero Trust Assessment Team Purpose: Provides tab completion for test selection in PowerShell cmdlets Supported Synonyms: - 'mfa', '2fa' → multifactor authentication, multi-factor authentication - 'ca' → conditional access - 'pim' → privileged identity management - 'sspr' → self service password reset - 'ad', 'aad' → active directory, azure active directory, entra Performance Notes: - Uses intelligent caching to avoid re-reading TestMeta.json on every completion - Cache automatically invalidates when TestMeta.json is modified - Optimized search algorithms for fast completion even with large test sets This file is automatically loaded when the ZeroTrustAssessmentV2 module is imported. The completion functionality is registered for both Invoke-ZtAssessment and Invoke-ZeroTrustAssessment cmdlets. #> # Zero Trust Assessment Argument Completers # Simplified cache for TestMeta content $script:TestMetaCache = @{ Content = $null; LastModified = $null } # Helper function to paginate completion results function Get-PaginatedCompletions { [CmdletBinding()] param( [Parameter(Mandatory)] [array]$CompletionResults, [int]$PageSize = 10 ) if ($CompletionResults.Count -le $PageSize) { return $CompletionResults } # Take first page $pagedResults = $CompletionResults | Select-Object -First $PageSize # Add pagination indicator $remainingCount = $CompletionResults.Count - $PageSize $paginationText = "... and $remainingCount more tests (use search to narrow results)" $paginationItem = [System.Management.Automation.CompletionResult]::new( '...', $paginationText, 'Text', "Total: $($CompletionResults.Count) tests available. Use search terms like 'mfa', 'high', 'access', etc. to filter results." ) return $pagedResults + $paginationItem } # Synonym mapping for common abbreviations $script:Synonyms = @{ 'mfa' = @('multifactor', 'multi-factor', 'authentication') '2fa' = @('two factor', 'multifactor', 'authentication') 'ca' = @('conditional access') 'pim' = @('privileged identity') 'sspr' = @('self service password') 'ad' = @('active directory', 'entra') 'aad' = @('azure active directory', 'entra') } function Get-ZtTestCompletion { [CmdletBinding()] param([string]$WordToComplete) try { # Get test metadata from the known location $testMetaPath = Join-Path -Path $script:ModuleRoot 'tests\TestMeta.json' if (-not (Test-Path $testMetaPath)) { return @() } # Check cache validity $fileInfo = Get-Item $testMetaPath if ($script:TestMetaCache.Content -and $script:TestMetaCache.LastModified -eq $fileInfo.LastWriteTime) { $testMetaContent = $script:TestMetaCache.Content } else { # Read and cache content $testMetaContent = Import-PSFJson -Path $testMetaPath $script:TestMetaCache = @{ Content = $testMetaContent; LastModified = $fileInfo.LastWriteTime } } # Generate completions with simplified matching logic $completions = foreach ($testProperty in $testMetaContent.PSObject.Properties) { $test = $testProperty.Value if ([string]::IsNullOrWhiteSpace($test.TestId) -or [string]::IsNullOrWhiteSpace($test.Title)) { continue } # Build search fields more robustly - always include Title, add others if they exist $searchFields = @($test.Title) if ($test.Category) { $searchFields += $test.Category } if ($test.RiskLevel) { $searchFields += $test.RiskLevel } $isMatch = $false $priority = 4 # Empty search matches all if ([string]::IsNullOrWhiteSpace($WordToComplete)) { $isMatch = $true $priority = 1 } # Exact TestId match elseif ($test.TestId.StartsWith($WordToComplete, [StringComparison]::OrdinalIgnoreCase)) { $isMatch = $true $priority = 1 } else { # Search in fields with synonym support $searchWords = $WordToComplete.Split(' ', [StringSplitOptions]::RemoveEmptyEntries) if ($searchWords.Count -gt 1) { # Multi-word: ALL words must be found across ANY fields (more intuitive) $allWordsFound = $true foreach ($word in $searchWords) { $wordFound = $false # Check if this word exists in any field foreach ($field in $searchFields) { if ($field.Contains($word, [StringComparison]::OrdinalIgnoreCase)) { $wordFound = $true break } } # Check synonyms if direct match fails if (-not $wordFound -and $script:Synonyms.ContainsKey($word.ToLower())) { foreach ($synonym in $script:Synonyms[$word.ToLower()]) { foreach ($field in $searchFields) { if ($field.Contains($synonym, [StringComparison]::OrdinalIgnoreCase)) { $wordFound = $true break } } if ($wordFound) { break } } } if (-not $wordFound) { $allWordsFound = $false break } } if ($allWordsFound) { $isMatch = $true $priority = 3 } } else { # Single word: check all fields $searchWord = $searchWords[0] foreach ($field in $searchFields) { if ($field.Contains($searchWord, [StringComparison]::OrdinalIgnoreCase)) { $isMatch = $true $priority = if ($test.Title.StartsWith($searchWord, [StringComparison]::OrdinalIgnoreCase)) { 2 } else { 3 } break } } # Check synonyms if no direct match if (-not $isMatch -and $script:Synonyms.ContainsKey($searchWord.ToLower())) { foreach ($synonym in $script:Synonyms[$searchWord.ToLower()]) { foreach ($field in $searchFields) { if ($field.Contains($synonym, [StringComparison]::OrdinalIgnoreCase)) { $isMatch = $true $priority = 3 break } } if ($isMatch) { break } } } } } if ($isMatch) { # Create tooltip and display text $tooltipParts = @($test.Title) if ($test.Category) { $tooltipParts += "Category: $($test.Category)" } if ($test.RiskLevel) { $tooltipParts += "Risk Level: $($test.RiskLevel)" } $listItemText = "$($test.TestId) - $($test.Title)" if ($test.Category) { $listItemText += " [$($test.Category)]" } [PSCustomObject]@{ CompletionResult = [System.Management.Automation.CompletionResult]::new( $test.TestId, $listItemText, 'ParameterValue', ($tooltipParts -join "`n") ) Priority = $priority NumericId = if ([int]::TryParse($test.TestId, [ref]$null)) { [int]$test.TestId } else { [int]::MaxValue } } } } # Sort results $sortedResults = $completions | Sort-Object Priority, NumericId | ForEach-Object CompletionResult # Apply pagination if we have more than 10 results return Get-PaginatedCompletions -CompletionResults $sortedResults } catch { Write-Debug "Error in Get-ZtTestCompletion: $($_.Exception.Message)" return @() } } # Main argument completer script block $ztTestsCompleterScript = { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters) # Handle comma-separated values and remove quotes $searchTerm = if ($wordToComplete.Contains(',')) { ($wordToComplete -split ',')[-1].Trim() } else { $wordToComplete } $searchTerm = $searchTerm.Trim('"', "'") # For Ctrl+Space scenarios, ensure we handle null/empty properly if ([string]::IsNullOrEmpty($searchTerm)) { $searchTerm = "" } return Get-ZtTestCompletion -WordToComplete $searchTerm } # Register argument completer for both--the cmdlet and its alias $commandNames = @( 'Invoke-ZtAssessment', 'Invoke-ZeroTrustAssessment' ) $commandNames | ForEach-Object { Register-ArgumentCompleter -CommandName $_ -ParameterName 'Tests' -ScriptBlock $ztTestsCompleterScript } |