src/Get-PsTree.ps1

#requires -version 6
using namespace System.Reflection
using namespace System.Runtime.InteropServices

Set-Alias -Name pstree -Value Get-PsTree
function Get-PsTree {
  [CmdletBinding()]
  param()

  begin {
    # CreateToolhelp32Snapshot, Process32First and Process32Next
    ($pinvoke = [PSObject].Assembly.GetType(
      'System.Management.Automation.PlatformInvokes'
    )).GetMethods([BindingFlags]'NonPublic, Static').Where{
      $_.Name -cmatch '\A(CreateT|Process)'
    }.ForEach{ Set-Variable -Name $_.Name -Value $_ }
    # TH32CS_SNAPPROCESS
    $Flags = 2 -as $pinvoke.GetNestedType(
      'SnapshotFlags', [BindingFlags]'NonPublic'
    )
    # helper function for extracting nested processes
    function Get-ChildProcess([PSCustomObject]$Process, [UInt16]$Depth = 1) {
      $pslist.Where{ $_.PPID -eq $Process.PID -and $_.PPID -ne 0 }.ForEach{
        "$("$([Char]32)" * 3 * $Depth)$($_.ImageName) ($($_.PID))"
        Get-ChildProcess $_ (++$Depth)
        $Depth--
      }
    }
  }
  process {}
  end {
    if (( # trying to take processes snapshot
      $snap = $CreateToolhelp32Snapshot.Invoke($null, @($Flags, [UInt32]0))
    ).IsInvalid) {
      Write-Verbose "[-] Could not get processes snapshot."
      return
    }
    # setting PROCESSENTRY32 size
    $PROCESSENTRY32 = [Activator]::CreateInstance(
      $pinvoke.GetNestedType('PROCESSENTRY32', [BindingFlags]'NonPublic')
    )
    $PROCESSENTRY32.dwSize = [Marshal]::SizeOf($PROCESSENTRY32)
    # extracting data fromsnapshot
    if ($Process32First.Invoke($null, (
      $ret = [Object[]]($snap, $PROCESSENTRY32)
    ))) {
      $pslist = do {
        [PSCustomObject]@{
          ImageName = $ret[1].szExeFile
          PID = $ret[1].th32ProcessId
          PPID = $ret[1].th32ParentProcessId
        }
      } while ($Process32Next.Invoke($null, (
        $ret = [Object[]]($snap, $PROCESSENTRY32)
      )))
      # build processes tree
      $pslist.Where{!(
        $p = Get-Process -Id $_.PPID -ErrorAction 0
      ) -or !$p.ProcessName -or $_.PPID -eq 0}.ForEach{
        "$($_.ImageName) ($($_.PID))"
        Get-ChildProcess $_
      }
    }
    else { Write-Verbose "[-] Could not parse snapshot data." }
    # release resources
    $snap.Dispose()
    [GC]::Collect()
  }
}