commands.psm1

# Enum defined as c# to make sure it gets exported by the module:
Add-Type -TypeDefinition @"
   [System.Flags]
   public enum PsoSearchType
   {
      CommandName = 1,
      Parameter = 2,
      Property = 4,
      Method = 8
   }
"@



function Test-ApplicationType
{
  <#
      .SYNOPSIS
      Returns the type of an application (architecture, console/gui)
 
      .DESCRIPTION
      Can be used to identify console-based commands and distinguish them from gui tools
      Analyzes the PE header so this cmdlet is probably specific for the Windows platform
      and won't work on other OS
 
      .PARAMETER Path
      Path to executable.
 
      .EXAMPLE
      Test-ApplicationType -Path c:\windows\explorer.exe
      returns the details about the specified executable
 
      .LINK
      https://github.com/TobiasPSP/PsCommandDiscovery
  #>



  param
  (
    [Parameter(Mandatory)]
    [string]
    $Path
  )
  
  $bytes = [Byte[]]::new(4096)
  $infos = [Ordered]@{}
  $infos['Name'] = [System.Io.Path]::GetFileNameWithoutExtension($Path)
  $infos['Extension'] = [System.IO.Path]::GetExtension($Path)
  $infos['Directory'] = Split-Path -Path $Path
  $infos['Type'] = 'Unknown'
  $infos['Architecture'] = 'Unknown'
  $infos['VersionInfo'] = (Get-Item -Path $Path).VersionInfo
  try
  {
    $stream = [System.IO.FileStream]::new($path, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read)
    if (($stream.Read($bytes, 0, 4096) -and $bytes[0] -eq 0x4d -and $bytes[1] -eq 0x5a) -eq $true) 
    {
      $offset = [system.bitconverter]::touint32($bytes, 60)
      $architecture  = [system.bitconverter]::touint16($bytes, $offset + 4)
      $appType    = [system.bitconverter]::touint16($bytes, $offset + 92)
      $infos['Architecture'] = switch ($architecture) {
        0x014c  { 'x86' }
        0x8664  { 'x64' }
      }
      $infos['Type'] = switch ($appType) {
        2  { 'Gui' }
        3  { 'Console' }
      }
    }
  }
  catch 
  {

  }
  finally
  {
    $stream.close()
  }

  return [pscustomobject]$infos | Add-Member -MemberType ScriptMethod -Name ToString -Value { 
    '{1}: {0} ({2}) [{3}] {4}' -f $this.Name, $this.Extension.ToLower(), $this.Architecture, $this.Type, $this.VersionInfo.ProductVersion
  } -Force -PassThru
}

function Convert-CimTypeToNetType
{
  <#
      .SYNOPSIS
      Converts string CimType to the corresponding .NET type
 
      .PARAMETER CimType
      Name of the CimType to convert
 
      .PARAMETER ReturnUnknownTypeAsAstring
      When there is no corresponding .NET type, return the CimType
 
      .EXAMPLE
      Convert-CimTypeToNetType -CimType 'SInt16'
      returns the .NET type representing signed 16bit integers.
 
      .LINK
      https://github.com/TobiasPSP/PsCommandDiscovery
  #>



  param
  (
    [String]
    [Parameter(Mandatory,ValueFromPipeline)]
    $CimType,
    
    [switch]
    $ReturnUnknownTypeAsAstring
  )
  
  begin
  {
    $lookup = @{
      UInt8 = [Byte]
      UInt16 = [UInt16]
      UInt32 = [UInt32]
      UInt64 = [UInt64]
      SInt8 = [SByte]
      SInt16 = [Int16]
      SInt32 = [int]
      SInt64 = [Int64]
      Real32 = [Single]
      Real64 = [Double]
      Boolean = [bool]
      DateTime = [DateTime]
      Char16 = [Char]
      String = [string]
    }
  }
  
  process
  {
    $result = $lookup[$CimType]
    if ($ReturnUnknownTypeAsAstring -and ($result -eq $null))
    {
      return $CimType
    }
    return $result
  }
}

function Convert-TypeToTypeAccelerator
{
  <#
      .SYNOPSIS
      Accepts any .NET type and returns the PowerShell string representation
 
      .DESCRIPTION
      Shortens .NET full type names with the short forms used by PowerShell:
      - returns the available PowerShell type accelerator
      - If no type accelerator is available, strips the "System." namespace from the type.
 
      .PARAMETER Type
      Type to convert
 
      .EXAMPLE
      Convert-TypeToTypeAccelerator -Type ([System.Xml.XmlDocument])
      returns the short PowerShell type accelerator "Xml"
 
      .EXAMPLE
      [System.Int32] | Convert-TypeToTypeAccelerator
      returns the short PowerShell type accelerator "int"
 
      .EXAMPLE
      Convert-TypeToTypeAccelerator -Type ([System.Int16])
      since there is no PowerShell type accelerator for this type, strips the default namespace
      and returns "Int16"
 
      .LINK
      https://github.com/TobiasPSP/PsCommandDiscovery
  #>



  param
  (
    [Type]
    [Parameter(Mandatory,ValueFromPipeline)]
    $Type
  )

  begin
  {
    if ($script:lookup -eq $null)
    {
      $typeaccelerators = [PSObject].Assembly.GetType('System.Management.Automation.TypeAccelerators')::Get
      $typeaccelerators.Keys | ForEach-Object { $lookup = @{} } { 
        $key = $typeaccelerators[$_]
        if ($lookup.ContainsKey($key))
        {
          if ($lookup[$key].Length -gt $_.length)
          {
            $lookup[$key] = $_
          }
        }
        else
        {
          $lookup[$key] = $_
        } 
      }
    }
  }
  
  process
  {
    $typeName = $lookup[$Type]
    if ([string]::IsNullOrEmpty($typename)) 
    {
      $typename = $_.FullName -replace '^System\.'
    }
    return $typeName
  }

}
function Find-PowerShellCommand
{
  <#
      .SYNOPSIS
      Helps discover PowerShell commands by looking for keywords in name and returned properties
 
      .DESCRIPTION
      Get-Command lets you find commands only based on command attributes such as name, module, or parameters.
      Often, a user searches for a command that will actually provide a given piece of information.
      Find-Command takes a keyword and looks at the actual results of a command.
      It then lists all commands that matches the keyword in
      - the command name
      - any property name
      - any property of any of the returned object types
      - any method of any of the returned object types
       
      .PARAMETER Keyword
      A string keyword describing what you are looking for, i.e. "user"
 
      .PARAMETER CommandType
      The type of command you are looking for, i.e. 'Cmdlet', 'Function', 'Alias', or 'Application'.
      You can specify multiple values as a comma-separated list
 
      .PARAMETER SearchType
      The scope of search that should be performed. Supported values are 'CommandName', 'Parameter', 'Property' and 'Method'
 
      .PARAMETER ShowProgress
      When specified, a progress bar is shown. This slows the command down considerably but may be helpful when queries take a long time.
 
      .EXAMPLE
      Find-PowerShellCommand -Keyword user
      Lists all cmdlets and functions that use the term "user" anywhere in its name, parameter, returned property or returned method.
 
      .EXAMPLE
      Find-PowerShellCommand -Keyword user -SearchType Property -ShowProgress
      Lists all commands that return objects that expose a property with "user" in its name, and shows a progress bar while searching.
 
      .EXAMPLE
      Find-PowerShellCommand -Keyword network -SearchType Parameter
      Finds all cmdlets and functions with a parameter that contains "network"
 
      .EXAMPLE
      Find-PowerShellCommand -Keyword power -CommandType Application
      Finds all executables and differentiates console commands from gui applications
 
      .EXAMPLE
      Find-PowerShellCommand -Keyword power -CommandType Application | Where-Object Type -eq Console | Select-Object -ExpandProperty Command
      Finds all console applications and lists version and source
      Again, all information returned is object-oriented so you can drill into the properties to retrieve what you need.
 
      .EXAMPLE
      Find-PowerShellCommand -Keyword Write -SearchType Method | Select-Object -Property Command, @{N='MethodName';E={$_.Member}}, @{N='Module';E={$_.Command.ModuleName}}
      Lists all commands that produce objects with methods that carry a "Write" in the method name.
      Note how you can use hashtables and calculated properties to combine information from different nest levels.
       
      .LINK
      https://github.com/TobiasPSP/PsCommandDiscovery
  #>



  param
  (
    [Parameter(Mandatory)]
    [string]
    $Keyword,
    
    [System.Management.Automation.CommandTypes[]] 
    $CommandType = 'Function,Cmdlet',
    
    [PsoSearchType]
    $SearchType = 'CommandName,Property',
    
    [switch]
    $ShowProgress
  
  )

  # get list of PowerShell type accelerators:
  $typeaccelerators = [PSObject].Assembly.GetType('System.Management.Automation.TypeAccelerators')::Get
  $typeaccelerators.Keys | ForEach-Object { $lookup = @{} } { $lookup[$typeaccelerators[$_]] = $_ }
 
  # whitelist of extensions considered an executable application
  $application = '.exe','.msc','.com','.bat','.vbs','.cpl'

  # get requested commands...
  if ($ShowProgress) { Write-Progress -Activity "Finding Commands for keyword '$keyword'" -Status 'Acquiring available commands' }
  $commands = Get-Command -CommandType $CommandType
  
  $commandCount = $commands.Count
  $currentCount = 0
  # select commands based on submitted keyword
  $commands | Foreach-Object {
    $command = $_
    $currentCount++
    
    if ($ShowProgress) 
    { 
      $percent = $currentCount * 100 / $commandCount
      Write-Progress "Finding Commands for keyword '$keyword'" -Status $command.Name -PercentComplete $percent
    }
      
    # check applications first if included in search:
    if ($Command.CommandType -eq 'Application')
    {
      # ignore extensions that cannot be directly called
      
      if ($_.Extension -in $application)
      {
        if ($_.Name -like "*$Keyword*")
        {
          $appInfo = Test-ApplicationType -Path $command.Path
          # command name matches keyword
          return  [PSCustomObject]@{
            Command = $command
            MatchType = 'Command'
            Member = $appInfo
            Type = $appInfo.Type
          }
        }
      }
    }
    else
    {
      # check whether any returned property matches the keyword:
      if ($Command.OutputType -and ($SearchType -band [PsoSearchType]::Property) -eq 'Property')
      {
        $command.OutputType | ForEach-Object {
          $outputType = $_
          # if the result is a .NET type...
          if ($outputType.Type -ne $null)
          {
            # examine all property names
            $outputType.Type.GetProperties() | ForEach-Object {
              if ($_.Name -like "*$keyword*")
              {
                # property name matches keyword
                $readonly = !$_.CanWrite
                           
                $propertyInfo = [PSCustomObject]@{
                  MemberName = $_.Name
                  Writeable = !$readonly
                  Type = $_.PropertyType
                  TypeName = $_.PropertyType | Convert-TypeToTypeAccelerator
                  PropertyInfo = $_
                } | Add-Member -MemberType ScriptMethod -Name ToString -Value { 
                  $note = if ($this.Writeable) { 'read/write' } else { 'readonly' }
                  '[{0}] {1} ({2})' -f $this.typename, $this.MemberName, $note 
                } -Force -PassThru


                [PSCustomObject]@{
                  Command = $command
                  MatchType = 'Property'
                  Member = $propertyInfo
                  Type = $outputType | Add-Member -MemberType ScriptMethod -Name ToString -Value { '[{0}]' -f $this.Type.FullName } -Force -PassThru
                }
              }
            }
          }
          # if type is $null then this is a WMI instance
          else
          {
            $wminame = $_.Name.Split('#')
            if ($wminame.Count -eq 2)
            {
              $namespace = Split-Path -Path $wminame[1]
              $classname = Split-Path -Path $wminame[1] -Leaf
              Get-CimClass -ClassName $classname -Namespace $namespace -ErrorAction Ignore |
              ForEach-Object {
                $wmiclass = $_
                $wmiclass.CimClassProperties| ForEach-Object {
                  if ($_.Name -like "*$keyword*")
                  {
                    $readonly = ($_.Flags -band 'ReadOnly') -eq 'ReadOnly'
                    $writeable = $_.Qualifiers.Where{$_.Name -eq 'write'}.Count -eq 1
                    $type = $_.CimType | Convert-CimTypeToNetType
                    $typename = if ($type -eq $null) { $_.CimType } else { $type | Convert-TypeToTypeAccelerator  }
                    $propertyInfo = [PSCustomObject]@{
                      MemberName = $_.Name
                      Writeable = !$readonly -or $writeable
                      Type = $type
                      TypeName = $typename 
                      PropertyInfo = $_
                    } | Add-Member -MemberType ScriptMethod -Name ToString -Value { 
                      $note = if ($this.Writeable) { 'read/write' } else { 'readonly' }
                      '[{0}] {1} ({2})' -f $this.TypeName, $this.MemberName, $note 
                    } -Force -PassThru
                  
                    # property name matches keyword
                    [PSCustomObject]@{
                      Command = $command
                      MatchType = 'WMIProperty'
                      Member = $propertyInfo
                      Type = $wmiclass
                    }
                  }
                }
              }
            }
          }
        }
      }
    
    
      # check whether any method in the returned object matches the keyword:
      if ($Command.OutputType -and ($SearchType -band [PsoSearchType]::Method) -eq 'Method')
      {
        $command.OutputType | ForEach-Object {
          $outputType = $_
          # if the result is a .NET type...
          if ($outputType.Type -ne $null)
          {
            # examine all property names
            $outputType.Type.GetMethods() | ForEach-Object {
              # does the method name match the keyword and is not a property getter/setter:
              if (($_.Name -like "*$keyword*") -and ($_.Name -notmatch '^(get_|set_)'))
              {
                $methodInfo = [PSCustomObject]@{
                  MemberName = $_.Name
                  Static = $_.IsStatic
                  Type = $_.ReturnType
                  TypeName = $_.ReturnType | Convert-TypeToTypeAccelerator
                  MethodInfo = $_
                } | Add-Member -MemberType ScriptMethod -Name ToString -Value { '[{0}] {1}(){2}' -f $this.TypeName, $this.MemberName, $(if($this.Static) { ' (static)' }) } -Force -PassThru


                [PSCustomObject]@{
                  Command = $command
                  MatchType = 'Method'
                  Member = $methodInfo
                  Type = $outputType | Add-Member -MemberType ScriptMethod -Name ToString -Value { '[{0}]' -f $this.Type.FullName } -Force -PassThru
                }
              }
            }
          }
          # if type is $null then this is a WMI instance
          else
          {
            $wminame = $_.Name.Split('#')
            if ($wminame.Count -eq 2)
            {
              $namespace = Split-Path -Path $wminame[1]
              $classname = Split-Path -Path $wminame[1] -Leaf
              Get-CimClass -ClassName $classname -Namespace $namespace -ErrorAction Ignore |
              ForEach-Object {
                $wmiclass = $_
                $wmiclass.CimClassMethods| ForEach-Object {
                  if ($_.Name -like "*$keyword*")
                  {
                    $isStatic = $_.Qualifiers.Where{$_.Name -eq 'static'}.Count -eq 1
                    $type = $_.ReturnType | Convert-CimTypeToNetType
                    $typename = if ($type -eq $null) { $_.ReturnType } else { $type | Convert-TypeToTypeAccelerator  }
                    $methodInfo = [PSCustomObject]@{
                      MemberName = $_.Name
                      Static = $isStatic
                      Type = $type
                      TypeName = $typename 
                      MethodInfo = $_
                    } | Add-Member -MemberType ScriptMethod -Name ToString -Value { 
                      '[{0}] {1} ({2})' -f $this.TypeName, $this.MemberName, $(if($this.Static) { ' (static)' }) 
                    } -Force -PassThru
                  
                    # method name matches keyword
                    [PSCustomObject]@{
                      Command = $command
                      MatchType = 'WMIMethod'
                      Member = $methodInfo
                      Type = $wmiclass
                    }
                  }
                }
              }
            }
          }
        }
      }
        
      # does command name match?
      if ($command.Name -like "*$Keyword*" -and (($SearchType -band [PsoSearchType]::CommandName) -eq 'CommandName'))
      {
        # command name matches keyword
        [PSCustomObject]@{
          Command = $command
          MatchType = 'CommandName'
          Member = $null
          Type = $null
        }
      }
    
      # does parameter name match?
      if (($SearchType -band [PsoSearchType]::Parameter) -eq 'Parameter')
      {
        if ($null -ne $command.Parameters -and (@($command.Parameters).Count -gt 0))
        {
          $command.Parameters.Keys | ForEach-Object {
            try
            {
              $parameter = $command.Parameters[$_]
            }
            catch
            {
              Wait-Debugger
            }
            if ($_ -like "*$keyword*")
            {
              $parameterInfo = [PSCustomObject]@{
                MemberName = $_
                Switch = $parameter.SwitchParameter
                Type = $parameter.ParameterType
                TypeName = $parameter.ParameterType | Convert-TypeToTypeAccelerator
                ParameterInfo = $parameter
              } | Add-Member -MemberType ScriptMethod -Name ToString -Value { '-{1} [{0}] {2}' -f $this.TypeName, $this.MemberName, $(if($this.Switch) { ' (Switch)' }) } -Force -PassThru
          
          
              # command name matches keyword
              [PSCustomObject]@{
                Command = $command
                MatchType = 'Parameter'
                Member = $parameterInfo
                Type = $command.ModuleName
              }
            }
          }
        }
      }
    }
  }
}