TM-WindowsUtility.psm1
using namespace System.Collections.Generic using namespace System.IO # Write verbose messages on import if ((Get-PSCallStack)[1].Arguments -imatch 'Verbose=True') { $PSDefaultParameterValues['*:Verbose'] = $true } # Ensure we're using the primary write commands from the Microsoft.PowerShell.Utility module. Set-Alias -Name 'Write-Progress' -Value 'Microsoft.PowerShell.Utility\Write-Progress' -Scope Script Set-Alias -Name 'Write-Debug' -Value 'Microsoft.PowerShell.Utility\Write-Debug' -Scope Script Set-Alias -Name 'Write-Verbose' -Value 'Microsoft.PowerShell.Utility\Write-Verbose' -Scope Script Set-Alias -Name 'Write-Host' -Value 'Microsoft.PowerShell.Utility\Write-Host' -Scope Script Set-Alias -Name 'Write-Information' -Value 'Microsoft.PowerShell.Utility\Write-Information' -Scope Script Set-Alias -Name 'Write-Warning' -Value 'Microsoft.PowerShell.Utility\Write-Warning' -Scope Script Set-Alias -Name 'Write-Error' -Value 'Microsoft.PowerShell.Utility\Write-Error' -Scope Script if ($env:OS -ne 'Windows_NT') { Write-Verbose 'TM-WindowsUtility should only be run on windows.' # Do not export any module commands. Export-ModuleMember return } # https://github.com/PowerShell/PowerShell/issues/17730#issuecomment-1190678484 $ExportedMembers = [List[string]]::new() $ExportedAliases = [List[string]]::new() Set-Alias -Name 'which' -Value 'where.exe' $ExportedAliases.Add('which') function Get-COMDetails { <# .SYNOPSIS Gets extended COM file properties. .DESCRIPTION This function retrieves extended file properties using COM (Component Object Model) and returns the Key/Value pairs in a PSCustomObject. .PARAMETER File The FileInfo object that has extended properties to analyze. #> [CmdletBinding()] [OutputType([PSCustomObject])] param ( [Parameter(Mandatory, ValueFromPipeline)] [Alias('Path', 'PSPath')] [Validation.ValidatePathExists('File')] [FileInfo]$File ) begin { $Output = [PSCustomObject]@{ FullName = $File.FullName } } process { $objShell = New-Object -ComObject Shell.Application $objFolder = $objShell.Namespace($File.DirectoryName) $objFile = $objFolder.ParseName($File.Name) if ($objFile) { for ($ColumnNumber = 0; $ColumnNumber -le 1000; $ColumnNumber++) { $ColumnName = $null $ColumnValue = $null $ColumnName = $objFolder.GetDetailsOf($null, $ColumnNumber) $ColumnValue = $objFolder.GetDetailsOf($ObjFile, $ColumnNumber) if ($ColumnValue) { # Ensure we always have a property name. if (-Not $ColumnName) { $ColumnName = "ColumnNumber$ColumnNumber" } $Output.PSObject.Properties.Add( [PSNoteProperty]::New($ColumnName, $ColumnValue) ) } } } } end { return $Output } } $ExportedMembers.Add('Get-COMDetails') function Get-DNSNamesBySubnet { <# .SYNOPSIS Attempts to retrieve DNS names for devices active within in a specified subnet. .DESCRIPTION This function queries DNS names for devices in a specified subnet and returns a list of objects containing their IPAddress, DeviceName, and ping statuses. .PARAMETER Subnet The subnet to query for DNS names. Should be a valid IP address. .PARAMETER CIDR The CIDR value for the subnet (default is 24). Acceptable values are between 24 and 30. .PARAMETER PassThru A switch that indicates that the output should be returned as a list of objects instead of a formatted table. .PARAMETER IncludeAllResults A switch that includes all results, including for IPs with empty DNS names. #> [CmdletBinding()] [OutputType([void], [PSCustomObject])] param ( [Parameter( Position = 0, Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName )] [Validation.ValidateIPv4Format()] [string]$IPV4Address, [Parameter( Position = 1, Mandatory = $false, ValueFromPipeline )] [ValidateRange(24, 30)] [int]$CIDR = 24, [Parameter(Mandatory = $false)] [switch]$PassThru, [Parameter(Mandatory = $false)] [switch]$IncludeAllResults ) begin { if ((Test-ApplicationExistsInPath -ApplicationName 'nslookup') -eq $false) { throw 'Get-DNSNamesBySubnet requires nslookup to run but it is not available in the PATH.' } $FirstThreeOctets = $IPV4Address -replace "$($IPV4Address.Split('.')[-1])$", '' [int]$FinalOctet = $IPV4Address -replace "^$FirstThreeOctets", '' $Output = [List[object]]::New() $CIDRAdressCount = switch ($CIDR) { 24 { 254 } 25 { 126 } 26 { 62 } 27 { 30 } 28 { 14 } 29 { 7 } 30 { 2 } } $End = $FinalOctet + $CIDRAdressCount if ($End -gt 255) { $End = 255 } } process { $JobOutput = $FinalOctet..$End | ForEach-Object -Parallel { try { $Address = $stdout = $DNSName = $null $Address = "$using:FirstThreeOctets$($using:FinalOctet + $_)" $null = . { nslookup $Address | Set-Variable stdout } 2>&1 | ForEach-Object ToString try { $selectStringSplat = @{ InputObject = $stdout Pattern = 'Name:' SimpleMatch = $true ErrorAction = 'Stop' } $DNSName = (Select-String @selectStringSplat)[0].line.split(':')[-1].trim() } catch { } if ($using:IncludeAllResults -or (-Not [string]::IsNullOrWhiteSpace($DNSName))) { return [PSCustomObject]@{ FinalOctet = ($using:FinalOctet + $_) IPAddress = $Address DeviceName = $DNSName Connection = (Test-Connection $Address) } } } catch { } } $JobOutput = $JobOutput | Sort-Object -Property FinalOctet foreach ($Job in $JobOutput) { $Output.Add( [PSCustomObject]@{ IPAddress = $Job.IPAddress DeviceName = $Job.DeviceName Ping01 = $Job.Connection[0].status Ping02 = $Job.Connection[1].status Ping03 = $Job.Connection[2].status Ping04 = $Job.Connection[3].status } ) } } end { if ($Output.Length -eq 0) { Write-Host 'No DNS names found.' } if ($PassThru) { return $Output } else { ($Output | Format-Table -Property *) } } } $ExportedMembers.Add('Get-DNSNamesBySubnet') function Get-ShortCutPath { <# .SYNOPSIS Gets the path to the target of a windows shortcut (.lnk / .url) file. .DESCRIPTION Uses wscript to retrieve the TargetPath and Arguments, if set, of a windows shortcut (.lnk / .url) file. .PARAMETER Path The path(s) to the shortcut file to analyze. #> [CmdletBinding()] [OutputType([System.String])] param( [Parameter( Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, Position = 0 )] [Alias('FullName')] [string[]]$Path ) begin { $wShell = New-Object -ComObject WScript.Shell } process { foreach ($Item in $Path){ $ItemPath = [IO.Path]::GetFullPath($Item) $ItemExt = [IO.Path]::GetExtension($ItemPath) $VerboseMessage = "Processing '$Item'" if ($Item -ne $ItemPath) { $VerboseMessage += "as '$ItemPath'" } Write-Verbose -Message "$VerboseMessage." if ((Test-Path $ItemPath -PathType Leaf) -eq $false) { Write-Error "Path '$ItemPath' is not a file that exists." continue } elseif (($ItemExt -ine '.lnk') -and ($ItemExt -ine '.url')) { Write-Error "Path '$ItemPath' does not have the required '.lnk' or '.url' extension!" continue } $shortCut = $wShell.CreateShortcut($ItemPath) $Result = $shortCut.TargetPath if ($shortCut.Arguments){ $Result += " $($shortCut.Arguments)" } Write-Output $Result } } } $ExportedMembers.Add('Get-ShortCutPath') function New-ConsoleArgWriter { <# .SYNOPSIS Creates a new 'ConsoleArgWriter' console application if it does not already exist. .DESCRIPTION The New-ConsoleArgWriter function generates an executable, ConsoleArgWriter.exe, in the specified $ShellBinPath. Use this executable as a mock for other executables when testing. ConsoleArgWriter.exe will write the input arguments to the console, rather than executing the actual application. This is useful for testing and debugging scripts that utilize command-line applications when you don't actually want to run the executables. This function checks if the 'ConsoleArgWriter.exe' already exists in the specified path. If it doesn't, it uses the Add-Type cmdlet to compile a new console application from a C# script. The new application simply writes the provided arguments to the console. To use the executable after running this function, alias the desired executable name to ConsoleArgWriter.exe. For example: Set-Alias -Name 'gpg.exe' -Value (Join-Path $ShellPath 'bin' 'ConsoleArgWriter.exe') & 'gpg.exe' --pinentry-mode=loopback "--d=$true" In this case, any arguments passed to 'gpg.exe' would be written to the console instead of being passed to the actual gpg.exe executable. .PARAMETER ShellBinPath The path where the ConsoleArgWriter.exe will be created. .EXAMPLE New-ConsoleArgWriter -ShellBinPath C:\temp This will create a new ConsoleArgWriter.exe in the C:\temp directory. .NOTES The New-ConsoleArgWriter function should be used for debugging and testing purposes. Avoid using it in production environments as it could potentially expose sensitive command-line arguments. #> [CmdletBinding()] param ( [Parameter(Mandatory)] [Validation.ValidatePathExists('Folder')] [string]$ShellBinPath ) $ArgWriter = Join-Path -Path $ShellBinPath -ChildPath 'ConsoleArgWriter.exe' if ((Test-Path -Path $ArgWriter) -eq $false) { if ($env:Path.Split([System.IO.Path]::PathSeparator) -notcontains $ShellBinPath) { Write-Warning ( "The ShellBinPath '$ShellBinPath' is not in your `$Env:Path. To use the ConsoleArgWriter you will need " + "to alias the entire path '$ArgWriter'." ) } # Starting this in Windows PowerShell because it can emit console applications. $CreateConsoleWriter = ([hashtable]@{ FilePath = 'powershell.exe' NoNewWindow = $true WorkingDirectory = $ShellBinPath ArgumentList = ( '-noprofile', '-ec', [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes(@' [hashtable]$ConsoleWriterSplat = @{ OutputType = 'ConsoleApplication' Language = 'CSharp' OutputAssembly = 'ConsoleArgWriter.exe' TypeDefinition = @" using System; public class Program { public static int Main(string[] args) { int count = 0; foreach (string arg in args) { if (null == arg) { Console.WriteLine("{0}: null", count); } else { Console.WriteLine("{0}: \"{1}\"", count, arg); } count++; } return 0; } } "@ } Add-Type @ConsoleWriterSplat '@))) }) Start-Process @CreateConsoleWriter } } $ExportedMembers.Add('New-ConsoleArgWriter') function touch { <# .SYNOPSIS Create a new file at the specified path. .DESCRIPTION The touch function emulates the Unix touch command and creates a new file at the specified path if it does not already exist. .PARAMETER Path The path where the new file will be created. #> [CmdletBinding()] [OutputType([Void])] param ( [Parameter( Mandatory, ValueFromPipeline, Position = 0 )] [string]$Path ) process { New-Item -ItemType File -Path $Path } } $ExportedMembers.Add('touch') function tail { <# .SYNOPSIS Display the last 'n' lines of a file. .DESCRIPTION The tail function emulates the Unix tail command and displays the last 'n' lines of a file. .PARAMETER Path The path of the file to display. .PARAMETER LiteralPath The literal path of the file to display. .PARAMETER follow A switch that indicates that the function should follow the file as it grows. .PARAMETER lines The number of lines to display from the end of the file. Default is 10. .EXAMPLE # Use Control C to exit the function and stop tailing the file. tail -f 'C:\Path\To\File.txt' #> [CmdletBinding()] [OutputType([Void])] param ( [Parameter( Mandatory, ValueFromPipeline, ParameterSetName = 'Path', Position = 0 )] [Validation.ValidatePathExists('File')] [FileInfo]$Path, [Parameter( Mandatory, ValueFromPipeline, ParameterSetName = 'LiteralPath', Position = 0 )] [Validation.ValidatePathExists('File')] [string]$LiteralPath, [Parameter(Mandatory = $false)] [switch]$follow, [Parameter(Mandatory = $false)] [Alias('n')] [int]$lines = 10 ) $ParameterSplat = @{} if ($Path) { $ParameterSplat.Add('Path', $Path) } if ($LiteralPath) { $ParameterSplat.Add('LiteralPath', $LiteralPath) } if ($follow) { $ParameterSplat.Add('Wait', $true) } $ParameterSplat.Add('Tail', $lines) Get-Content @ParameterSplat } $ExportedMembers.Add('tail') function Search-Registry { <# .SYNOPSIS Search the Windows Registry for key names, key value names, or key value data matching specified criteria. .DESCRIPTION The Search-Registry function allows you to search the Windows Registry for - Key names - Value names - Value data It outputs PSCustomObjects that contain the key, matched content type (KeyName, ValueName, or ValueData), and the matching content. .PARAMETER Path The registry path to start the search from. .PARAMETER Recurse Indicates whether to search in subkeys. .PARAMETER SearchRegex The regular expression used as the search criteria. .PARAMETER KeyName Used in conjunction with the SearchRegex parameter. Specifies that key names will be tested. By default, when the SingleSearchString switches have not been specified, all content types are evaluated. When using this switch, only the specified type(s) (KeyName, ValueName, and/or ValueData) will be evaluated. .PARAMETER ValueName Used in conjunction with the SearchRegex parameter. Specifies that the value names will be tested. By default, when the SingleSearchString switches have not been specified, all content types are evaluated. When using this switch, only the specified type(s) (KeyName, ValueName, and/or ValueData) will be evaluated. .PARAMETER ValueData Used in conjunction with the SearchRegex parameter. Specifies that the value data will be tested. By default, when the SingleSearchString switches have not been specified, all content types are evaluated. When using this switch, only the specified type(s) (KeyName, ValueName, and/or ValueData) will be evaluated. .PARAMETER KeyNameRegex Specifies a regex that will be checked against key names only. .PARAMETER ValueNameRegex Specifies a regex that will be checked against value names only. .PARAMETER ValueDataRegex Specifies a regex that will be checked against value data only. .EXAMPLE # Search for key names matching "Microsoft," "Adobe," or "Google" under the specified registry path that has value # data consisting of only numeric characters. Search-Registry -Path HKLM:\SOFTWARE -KeyNameRegex "Microsoft|Adobe|Google" -ValueDataRegex "^\d+$" .EXAMPLE # Recursively search for value names matching "DNS*" or "IP*" under the given path, including subkeys. Search-Registry -Path HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters -Recurse -ValueNameRegex "DNS*|IP*" .EXAMPLE # Search for key names containing the word "Security" under the specified registry path. Search-Registry -Path HKCU:\Software -KeyNameRegex "Security" .EXAMPLE # Search for keys with empty value data (no data) under the specified registry path. Search-Registry -Path HKCU:\Software -ValueDataRegex "^$" .EXAMPLE # Search for value names containing "Program Files" under the specified registry path. Search-Registry -Path HKLM:\SOFTWARE -ValueNameRegex "Program Files" .EXAMPLE # Find key names that match "ProgramKeyName". This will not evaluate either ValueNames or ValueData. Search-Registry -Path HKLM:\SOFTWARE -SearchRegex "ProgramKeyName" -KeyName #> [CmdletBinding()] param ( [Alias('PsPath')] [Parameter(Mandatory, Position = 0, ValueFromPipelineByPropertyName)] [string[]]$Path, [Parameter(Mandatory = $false)] [switch]$Recurse, [Parameter(ParameterSetName = 'SingleSearchString', Mandatory)] [string]$SearchRegex, [Parameter(ParameterSetName = 'SingleSearchString', Mandatory = $false)] [switch]$KeyName, [Parameter(ParameterSetName = 'SingleSearchString', Mandatory = $false)] [switch]$ValueName, [Parameter(ParameterSetName = 'SingleSearchString', Mandatory = $false)] [switch]$ValueData, [Parameter(ParameterSetName = 'MultipleSearchStrings', Mandatory = $false)] [string]$KeyNameRegex, [Parameter(ParameterSetName = 'MultipleSearchStrings', Mandatory = $false)] [string]$ValueNameRegex, [Parameter(ParameterSetName = 'MultipleSearchStrings', Mandatory = $false)] [string]$ValueDataRegex ) begin { if ($PSCmdlet.ParameterSetName -eq 'SingleSearchString') { $NoSwitchesSpecified = $false -eq ( $PSBoundParameters.ContainsKey('KeyName') -or $PSBoundParameters.ContainsKey('ValueName') -or $PSBoundParameters.ContainsKey('ValueData') ) if ($KeyName -or $NoSwitchesSpecified) { $KeyNameRegex = $SearchRegex } if ($ValueName -or $NoSwitchesSpecified) { $ValueNameRegex = $SearchRegex } if ($ValueData -or $NoSwitchesSpecified) { $ValueDataRegex = $SearchRegex } } } process { foreach ($CurrentPath in $Path) { foreach ($Key in (Get-ChildItem $CurrentPath -Recurse:$Recurse)) { if ($KeyNameRegex) { Write-Verbose "$($Key.Name): Checking KeyNamesRegex" if ($Key.PSChildName -match $KeyNameRegex) { Write-Verbose "$($Key.Name): Found KeyName match for `"$KeyNameRegex`"!" Write-Output -InputObject ([PSCustomObject]@{ Key = $Key ContentMatch = 'KeyName' MatchContent = $Key.PSChildName } ) } } if ($ValueNameRegex -or $ValueDataRegex) { foreach ($KeyValueName in $Key.GetValueNames()) { if ($ValueNameRegex) { Write-Verbose "$($Key.Name): Checking ValueNamesRegex" if ($KeyValueName -match $ValueNameRegex) { Write-Verbose "$($Key.Name): Found ValueName match for `"$ValueNameRegex`"!" Write-Output -InputObject ([PSCustomObject]@{ Key = $Key ContentMatch = 'ValueName' MatchContent = $KeyValueName } ) } } if ($ValueDataRegex) { Write-Verbose "$($Key.Name): Checking ValueDataRegex" $KeyValueData = $Key.GetValue($KeyValueName) if ($KeyValueData -match $ValueDataRegex) { Write-Verbose "$($Key.Name): Found ValueData match for `"$ValueDataRegex`"!" Write-Output -InputObject ([PSCustomObject] @{ Key = $Key ContentMatch = 'ValueData' MatchContent = $KeyValueData } ) } } } } } } } } $ExportedMembers.Add('Search-Registry') function Set-WindowFocus { <# .SYNOPSIS Sets the focus on a specified window. .DESCRIPTION This function shifts focus to a specified window by utilizing either the process name or the process object. By invoking Windows APIs, the function manages the visibility to show or hide GUI process windows. .PARAMETER ProcessName The name of the process whose window you want to focus. .PARAMETER Process The process object whose window you want to focus. .PARAMETER Minimize A switch that indicates if the window should be minimized after setting the focus. #> [CmdletBinding()] [OutputType([Void])] param ( [Parameter( Mandatory, ValueFromPipeline, ParameterSetName = 'ProcessName', Position = 0 )] [string]$ProcessName, [Parameter( Mandatory, ValueFromPipeline, ParameterSetName = 'Process', Position = 0 )] [ComponentModel.Component]$Process, [Parameter(Mandatory = $false)] [switch]$Minimize ) # https://stackoverflow.com/a/58548853 Add-Type -Namespace ProfileUtility -Name WindowVisibility -MemberDefinition @' [DllImport("user32.dll", SetLastError=true)] public static extern bool SetForegroundWindow(IntPtr hWnd); [DllImport("user32.dll", SetLastError=true)] public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); [DllImport("user32.dll", SetLastError=true)] public static extern bool IsIconic(IntPtr hWnd); '@ switch ($PSCmdlet.ParameterSetName) { 'ProcessName' { $ProcessName = $ProcessName -replace '\.exe$' $Processes = (Get-Process -ErrorAction Ignore $ProcessName) if ($Processes.Count -gt 1) { $Index = 0 $ProcessList = Foreach ($Process in $Processes) { [PSCustomObject]@{ Number = $Index WindowTitle = $Process.MainWindowTitle } ++$Index } $PromptAnswer = Read-Host -Prompt ( "Found multiple processes for '$ProcessName'. Which would you like to select.`n" + ($ProcessList | Format-Table | Out-String) ) $hWnd = $Processes[$PromptAnswer].MainWindowHandle } else { try { $hWnd = $Processes[0].MainWindowHandle } catch { <# Do Nothing #> } } } 'Process' { $hWnd = $Process.MainWindowHandle } } if (-not $hWnd) { Throw 'Failed to retrieve MainWindowHandle.' } # https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-showwindow if ($Minimize) { [ProfileUtility.WindowVisibility]::ShowWindow($hwnd, 6) | Out-Null } else { # Set the focus on the window. [ProfileUtility.WindowVisibility]::SetForegroundWindow($hWnd) | Out-Null # If the window is minimized, restore it. if ([ProfileUtility.WindowVisibility]::IsIconic($hwnd)) { [ProfileUtility.WindowVisibility]::ShowWindow($hwnd, 9) | Out-Null } } } $ExportedMembers.Add('Set-WindowFocus') function Wait-Active { <# .SYNOPSIS Keeps the computer active for a specified amount of time. .DESCRIPTION This function opens Notepad and sends a key press to keep the computer active. The duration can be specified in minutes, as a TimeSpan, or as a DateTime. .PARAMETER StopTime The DateTime at which the function should stop keeping the computer active. .PARAMETER Duration The TimeSpan duration to keep the computer active. .PARAMETER Minutes The number of minutes to keep the computer active. .PARAMETER SleepSeconds The number of seconds to sleep between each activity (default is 60 seconds). #> [CmdletBinding(DefaultParameterSetName = 'Minutes')] [OutputType([Void])] param ( [Parameter( Position = 0, ParameterSetName = 'DateTime', Mandatory = $false, ValueFromPipeline, ValueFromPipelineByPropertyName )] [DateTime]$StopTime = (Get-Date).AddMinutes(5), [Parameter( Position = 0, ParameterSetName = 'TimeSpan', Mandatory = $false, ValueFromPipeline, ValueFromPipelineByPropertyName )] [ValidateRange(0, [int]::MaxValue)] [TimeSpan]$Duration = (New-TimeSpan -Start (Get-Date) -End (Get-Date).AddMinutes(5)), [Parameter( Position = 0, ParameterSetName = 'Minutes', Mandatory = $false, ValueFromPipeline, ValueFromPipelineByPropertyName )] [ValidateRange(0, [int]::MaxValue)] [int]$Minutes = 5, [Parameter( Position = 1, Mandatory = $false, ValueFromPipelineByPropertyName )] [ValidateRange(0, [int]::MaxValue)] [Int]$SleepSeconds = 60 ) $WaitTill = switch ($PSCmdlet.ParameterSetName) { 'DateTime' { $StopTime } 'TimeSpan' { (Get-Date).AddSeconds($timespan.TotalSeconds) } 'Minutes' { (Get-Date).AddMinutes($Minutes) } } $Process = Start-Process 'Notepad.exe' -PassThru Start-Sleep -Seconds 5 do { if ((Get-Process -InputObject $Process).HasExited) { Write-Warning 'Notepad has exited.' return } Set-WindowFocus $Process (New-Object -ComObject wscript.shell).SendKeys('.') Set-WindowFocus $Process -Minimize Start-Sleep -Seconds $SleepSeconds $CurrentTime = Get-Date } while ($CurrentTime -lt $WaitTill) Stop-Process $Process } $ExportedMembers.Add('Wait-Active') Export-ModuleMember -Function $ExportedMembers.ToArray() -Alias $ExportedAliases.ToArray() |