Public/Helpers/Show-BlackCatCommands.ps1

function Show-BlackCatCommands {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$false, Position=0)]
        [Alias("cat", "c")]
        [ArgumentCompleter({
            param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)

            # Get the Public folder path
            $publicFolderPath = Split-Path -Parent $PSScriptRoot

            # Get immediate subdirectories of Public (excluding Helpers)
            $categories = Get-ChildItem -Path $publicFolderPath -Directory |
                         Where-Object { $_.Name -ne 'Helpers' } |
                         Where-Object {
                             # Only return directories that contain at least one PowerShell script
                             (Get-ChildItem -Path $_.FullName -Filter "*.ps1" -File).Count -gt 0
                         } |
                         Select-Object -ExpandProperty Name |
                         Where-Object { $_ -like "$wordToComplete*" }

            return $categories
        })]
        [string]$Category
    )

    try {
        Clear-Host

        # The module root is two levels up from the Helpers folder (since Helpers is inside Public)
        $moduleRoot = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent
        $psd1Path = Join-Path -Path $moduleRoot -ChildPath "BlackCat.psd1"
        Write-Verbose "Looking for BlackCat.psd1 at $psd1Path"
        if (-not (Test-Path $psd1Path)) {
            throw "BlackCat.psd1 not found at $psd1Path"
        }

        $moduleManifest = Import-PowerShellDataFile -Path $psd1Path


        if (-not $moduleManifest.ContainsKey('FileList')) {
            throw "No 'FileList' key found in BlackCat.psd1"
        }

        $fileList = $moduleManifest['FileList']

        $logo = @"
 
                        /\_/\
                       ( ◣_◢ )
                        > ^ <
     __ ) ___ | | | | ___| __ \ | /\_/\
     __ \ / | | __| | / | / _` | __| ( ◣_◢ )
     | | / ___ __| ( < | | ( | | > ^ <
    ____/ _/ _| \___| _|\_\ \____| \ \__,_| \__| ( )
                                             \____/
 
        $updateMessage
 
"@

        Write-Host $logo -ForegroundColor Blue

        $results = @()
        $fileCount = 0

        foreach ($filePath in $fileList) {
            if ($Category) {
                # Normalize path separators for comparison
                $normalizedCategory = $Category -replace '[\\/]', [IO.Path]::DirectorySeparatorChar
                if ($filePath -notlike "*$normalizedCategory*") {
                    continue
                }
            }
            $fileName = [IO.Path]::GetFileNameWithoutExtension((Split-Path -Path $filePath -Leaf))
            $fileCount++

            # Get full path to the file
            $fullFilePath = Join-Path -Path $moduleRoot -ChildPath $filePath
            $description = "No description available"

            # Try to extract description from the file
            if (Test-Path $fullFilePath) {
                # First attempt to load the function and get its help
                try {
                    # Get just the function name without path
                    $functionName = [System.IO.Path]::GetFileNameWithoutExtension($fullFilePath)
                    # Try to get help for the function (only works if loaded)
                    $helpInfo = Get-Help -Name $functionName -ErrorAction SilentlyContinue
                    
                    if ($helpInfo -and $helpInfo.Synopsis) {
                        $description = $helpInfo.Synopsis.Trim()
                    }
                }
                catch {
                    # Fallback to parsing the file directly
                    Write-Verbose "Couldn't get help for $functionName, falling back to file parsing"
                }
                
                # Always parse the file directly for more reliable extraction
                # Prioritize SYNOPSIS as the default description
                $content = Get-Content -Path $fullFilePath -Raw
                $fileDescription = "No description available"
                
                # Special handling for Export-AzAccessToken.ps1
                if ($fileName -eq "Export-AzAccessToken") {
                    $fileDescription = "The Export-AzAccessToken function retrieves access tokens."
                }
                else {
                    # Prioritize .SYNOPSIS as the default source
                    if ($content -match '\.SYNOPSIS\s*\r?\n\s*(.*?)(?:\r?\n\s*\.|\r?\n\s*\r?\n|$)') {
                        $fileDescription = $matches[1].Trim()
                    }
                    # Fall back to .DESCRIPTION if SYNOPSIS not found
                    elseif ($content -match '<#(?:.|\n)*?\.DESCRIPTION\s*\r?\n\s*(.*?)(?:\r?\n\s*\.|\r?\n\s*\r?\n|\r?\n\s*#>|$)') {
                        $fileDescription = $matches[1].Trim()
                    }
                    # Try alternative pattern for help blocks
                    elseif ($content -match '\.DESCRIPTION\s*(.*?)(?:\r?\n\s*\.|\r?\n\s*\r?\n|$)') {
                        $fileDescription = $matches[1].Trim()
                    }
                    # If neither found, try to find first comment that might serve as description
                    elseif ($content -match '#\s*(.*?)(\r?\n|$)') {
                        $fileDescription = $matches[1].Trim()
                    }
                }
                
                # Use file-parsed description
                if ($fileDescription -ne "No description available") {
                    $description = $fileDescription
                }
                
                # Clean up multi-line descriptions - replace newlines with spaces
                $description = $description -replace '\s*\r?\n\s*', ' '
                
                # Enforce 83-character limit (with "..." if truncated)
                if ($description.Length -gt 83) {
                    $description = $description.Substring(0, 80) + "..."
                }
            }

            $results += [PSCustomObject]@{
                Function = $fileName
                Description = $description
            }
        }

        Write-Verbose "Found $fileCount public functions"

        $results | Format-Table -AutoSize

        Write-Host "========== Summary ==========`n" -ForegroundColor Cyan
        Write-Host "Found $fileCount public functions`n" -ForegroundColor White
    }
    catch {
        Write-Error "Error processing BlackCat.psd1: $_"
    }
    <#
    .SYNOPSIS
        Displays all available BlackCat functions organized by MITRE ATT&CK category.
 
    .DESCRIPTION
        This function shows a comprehensive list of all public functions in the BlackCat module,
        organized by their MITRE ATT&CK tactic category. You can filter by category to show
        only functions related to specific attack phases.
 
    .PARAMETER Category
        Optional category to filter functions. If not specified, shows all categories.
        Auto-completion is provided for available categories (e.g., Discovery, Persistence, Credential Access).
 
    .EXAMPLE
        Show-BlackCatCommands
        Shows all available BlackCat functions organized by category.
 
    .EXAMPLE
        Show-BlackCatCommands -Category Discovery
        Shows only functions in the Discovery category.
 
    .ALIAS
        cat, c
 
    .NOTES
        This is a utility/support function and does not directly map to MITRE ATT&CK tactics.
        Use this function to explore available BlackCat capabilities and their classifications.
    #>

}