# Copyright (c) Microsoft Corporation. # Licensed under the MIT license. <# Convert-Etl2Pcapng A PowerShell wrapper for etl2pcapng, which converts packet captures in ETL format generated by ndiscap (the ETW provider in Windows that produces packet capture events) to pcapng format (readable by Wireshark). To-DO: - DONE - ISSUE 12 (GI12): Force a unregister/register on version change - Unregister deletes settings, so I don't want to do this. - Instead I added logic to update settings on version change. - In the future, I can add an update function that automates any changes. - ISSUE 13 (GI12): Add check for wt.exe - DONE - ISSUE 14/15 (GI15): *-Etl2pcapng fails to write settings - DONE - ISSUE 16 (GI16): Add pktmon support. - ISSUE 17 (GI17): Fix right-click context menu for Windows 11 - #> ### CONSTANTS ### # version of Convert-Etl2Pcapng $script:CurrentE2PVersion = "2025.12.0001" ################ ### ### ### PUBLIC ### ### ### ################ #region PUBLIC # FUNCTION : Register-Etl2Pcapng # PURPOSE : Registers the ecript to ETL files function Register-Etl2Pcapng { <# .SYNOPSIS Adds a right-click menu option in Windows for etl2pcapng. .DESCRIPTION Registers a shell context menu item for Convert-Etl2Pcapng. Right-clicking on an ETL fill will show an option "Convert with etl2pcapng". This will execute Convert-Etl2Pcapng with default settings against the ETL file. .EXAMPLE Register-Etl2Pcapng Registers the "Convert with etl2pcapng" shell menu item. .NOTES Author: Microsoft Edge OS Networking Team and Microsoft CSS Please file issues on GitHub @ https://github.com/microsoft/Convert-Etl2Pcapng .LINK More projects : https://github.com/topics/msftnet Windows Networking Blog : https://blogs.technet.microsoft.com/networking/ #> [CmdletBinding()] param ( # Causes the explorer menu option, "Convert with etl2pcapng", to not exit the command prompt when complete and output Verbose logging. [switch]$UseVerbose, # Causes the explorer menu option, "Convert with etl2pcapng", to not exit the command prompt when complete and output Debug logging. [switch]$UseDebug, # Accepts the EULA at runtime and prevents the prompt to accept. [switch]$AcceptEULA ) Write-Verbose "Register-Etl2Pcapng - Work! Work!" Write-Verbose "Convert-Etl2Pcapng - Using PowerShell Version $($PSVersionTable.PSVersion)" # test for Admin access Write-Verbose "Register-Etl2Pcapng - Test admin rights." if (-NOT ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { return (Write-Error "Register-Etl2Pcapng: Administrator rights are needed to execute this command. Please run PowerShell as Administrator and try again." -EA Stop) } # read settings.xml $script:settings = Get-E2PSettings # for some reason the first pass doesn't return a E2PSettings class, so we force the issue if ($settings -isnot [E2PSettings]) { $settings = [E2PSettings]::new($settings) } Write-Verbose "Register-Etl2Pcapng - Settings:`n$($settings.ToString())" # EULA must be accepted to proceed # don't prompt if -AcceptEULA was set if ($AcceptEULA.IsPresent) { Write-Verbose "Register-Etl2Pcapng - Accepting EULA by parameter." $settings.SetEulaStatus($true) Set-E2PSettings $settings } # prompt if no -AcceptEULA and EULA in settings is set to false elseif ($settings.AcceptEULA -eq $false) { Write-Host @" Privacy Notice and End User License Agreement (EULA) This PowerShell module does not collect or upload data to Microsoft, third-parties, or Microsoft partners. Tracking and other statistical website data may be collected by PowerShellGallery.com when the module is downloaded, and by Github.com when the etl2pcapng.zip file is downloaded or updated by the module during cmdlet execution. By agreeing to the EULA you permit the Convert-Etl2Pcapng module to contact github.com to check, download, and extract etl2pcapng to this computer from github.com. WARNING: You must accept the EULA to register Convert-Etl2Pcapng as a context menu item. "@ $c = 0 do { $answer = Read-Host "[Y] Yes, I Agree `n[N] I do Not agree`nResponse" $c++ } until ($answer -eq 'a' -or $answer -eq 'y' -or $answer -eq 'n' -or $c -gt 3) switch ($answer) { 'y' { Write-Verbose "Register-Etl2Pcapng - Accepting EULA by user input." # need to write EULA and make sure the file is downloaded $settings.SetEulaStatus($true) Set-E2PSettings $settings break } 'n' { return $null } default { return (Write-Error "Failed to get a valid user response to the EULA." -EA Stop)} } } # make sure the module is installed, just in case Write-Verbose "Register-Etl2Pcapng: Verify this is running from the module." $isModFnd = Get-Module -ListAvailable Convert-ETL2PCAPNG -EA SilentlyContinue if (-NOT $isModFnd) { # is it running and not in the list for some reason (like during testing)? $isModFnd = Get-Module Convert-ETL2PCAPNG -EA SilentlyContinue if (-NOT $isModFnd) { return (Write-Error "This cannot be run outside of the Convert-ETL2PCAPNG module. Please install the Convert-ETL2PCAPNG module first:`n`nInstall-Module Convert-ELT2PCAPNG." -EA Stop) } } # create a PSDrive to HKEY_CLASSES_ROOT Write-Verbose "Register-Etl2Pcapng: Create PSDrive for HKEY_CLASSES_ROOT." if (-NOT (Get-PSDrive -Name HKCR -EA SilentlyContinue)) { New-PSDrive -Name HKCR -PSProvider Registry -Root HKEY_CLASSES_ROOT -Scope Local | Out-Null } # remove existing Convert-Etl2Pcapng entries $rootPath = "HKCR:\SystemFileAssociations\.etl\shell\Convert-Etl2Pcapng" $isCE2PReg = Get-Item $rootPath -EA SilentlyContinue if ($isCE2PReg) { try { Remove-Item $rootPath -Force -Recurse -EA Stop } catch { Write-Warning "Failed to cleanup older Convert-Etl2Pcapng registration. This is a non-terminating warning." } } # create the shell extension for etl2pcapng Write-Verbose "Register-Etl2Pcapng: Add the Convert-Etl2Pcapng app to HKCR:\SystemFileAssociations\.etl to prevent possible conflicts." if (-NOT (New-RegKey "$rootPath\Command" Directory)) { Write-Error "Could not create directory in SystemFileAssociations."; exit } # create the command Write-Verbose "Register-Etl2Pcapng - Configure Convert-Etl2Pcapng." if (-NOT (New-RegKey $rootPath -value "Convert with etl2pcapng")) { Write-Error "Could not write menu text." } # create Open With keys for better Win11 support $rootOpenWith = "HKCR:\.etl\OpenWithProgids" $progID = "Convert-Etl2Pcapng.etl" Write-Verbose "Register-Etl2Pcapng - Configure Convert-Etl2Pcapng for Open With menu." $isOpenWith = Get-Item $rootOpenWith -EA SilentlyContinue try { # if the OpenWithProgids is not there then create it if (-NOT $isOpenWith) { New-Item $rootOpenWith -Force -EA Stop } # The set version will create or change, where new only creates, so use set. Set-ItemProperty -Path $rootOpenWith -Name $progID -Type String -Value '' -Force -EA Stop } catch { Write-Error "Failed to create the Open With association to .etl: $_" } # discover the current shell since currentuser scope puts the module in different places in 7+ than 5.1. # powershell = Windows PowerShell 5.1 # pwsh = PowerShell 7+ if ($host.Version.Major -eq 5) { Write-Verbose "Register-Etl2Pcapng - Detected Windows PowerShell." $cli = "powershell.exe" } else { Write-Verbose "Register-Etl2Pcapng - Detected PowerShell 7." $cli = "pwsh.exe" } # if this is Win11 then we can use Windows Terminal (wt) if ( [System.Environment]::OSVersion.Version.Build -ge 22000 ) { Write-Verbose "Register-Etl2Pcapng - Detected Windows 11. Using Windows Terminal as the base console." if ($UseVerbose) { $cmd = "wt $cli -NoProfile -Command Convert-Etl2Pcapng '`"%1`"' -Pause -Verbose" } elseif ($UseDebug) { $cmd = "wt $cli -NoProfile -Command Convert-Etl2Pcapng '`"%1`"' -Pause -Debug -Verbose" } else { $cmd = "wt $cli -NoProfile -Command Convert-Etl2Pcapng '`"%1`"'" } } else { Write-Verbose "Register-Etl2Pcapng - Detected pre-Windows 11. Using CMD as the base console." if ($UseVerbose) { $cmd = "cmd /k $cli -NoProfile -NonInteractive -NoLogo -Command Convert-Etl2Pcapng '%1' -Verbose" } elseif ($UseDebug) { $cmd = "cmd /k $cli -NoProfile -NonInteractive -NoLogo -Command Convert-Etl2Pcapng '%1' -Debug -Verbose" } else { $cmd = "cmd /c $cli -NoProfile -NonInteractive -NoLogo -Command Convert-Etl2Pcapng '%1'" } } Write-Verbose "Register-Etl2Pcapng: Add Convert-ETL2PCAPNG command: $cmd" if (-NOT (New-RegKey "$rootPath\Command" -Value $cmd)) { Write-Error "Could not write command to registry."; exit } # create the progID key structure and add the command for the open with menu $rootProgID = "HKCR:\$progID\shell\Convert with etl2pcapng\command" try { $null = New-Item $rootProgID -Force -EA Stop Set-ItemProperty -Path $rootProgID -Name '(Default)' -Type String -Value $cmd -Force -EA Stop Set-ItemProperty -Path "HKCR:\$progID\shell\Convert with etl2pcapng" -Name '(Default)' -Type String -Value "Convert with etl2pcapng" -Force -EA Stop } catch { # remove the OpenWithProgids entry $null = Remove-ItemProperty -Path $rootOpenWith -Name $progID -Force -EA SilentlyContinue Write-Error "Failed to create the Open With app: $_" } # check for the ETL extenstion in HKCR, create if missing Write-Verbose "Register-Etl2Pcapng: Add the context item to .etl files." Write-Verbose "Register-Etl2Pcapng: Work complete!" } #end Register-Etl2Pcapng # FUNCTION : Unregister-Etl2Pcapng # PURPOSE : Unregisters the ecript to ETL files function Unregister-Etl2Pcapng { [CmdletBinding()] param( # Does not delete settings.xml and etl2pcapng.exe during the unregistration process. [Parameter()] [switch] $NoClobber ) <# .SYNOPSIS Removes the right-click menu option in Windows for etl2pcapng. .DESCRIPTION Unregisters the shell context menu item for Convert-Etl2Pcapng. This will remove the option to right-click on an ETL file and select "Convert with etl2pcapng". .EXAMPLE Unregister-Etl2Pcapng Unregisters the "Convert with etl2pcapng" menu item. .NOTES Author: Microsoft Edge OS Networking Team and Microsoft CSS Please file issues on GitHub @ https://github.com/microsoft/Convert-Etl2Pcapng .LINK More projects : https://github.com/topics/msftnet Windows Networking Blog : https://blogs.technet.microsoft.com/networking/ #> Write-Verbose "Unregister-Etl2Pcapng: Work! Work!" Write-Verbose "Convert-Etl2Pcapng - Using PowerShell Version $($PSVersionTable.PSVersion)" # test for Admin access Write-Verbose "Unregister-Etl2Pcapng: Test admin rights." if (-NOT ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { Write-Error "Unregister-Etl2Pcapng: Administrator rights are needed to execute this command. Please run PowerShell as Administrator and try again." return $null } # create a PSDrive to HKEY_CLASSES_ROOT Write-Verbose "Unregister-Etl2Pcapng: Creating HKCR PSDrive." if (-NOT (Get-PSDrive -Name HKCR -EA SilentlyContinue)) { New-PSDrive -Name HKCR -PSProvider Registry -Root HKEY_CLASSES_ROOT -Scope Local | Out-Null } # check if the original version was registered and remove its components # remove the Convert-ETL2PCAPNG HKCR key $test = Get-Item "HKCR:\Convert-Etl2Pcapng" -EA SilentlyContinue if ($test) { Write-Verbose "Unregister-Etl2Pcapng: Removing Convert-Etl2Pcapng HKCR app." Remove-Item "HKCR:\Convert-Etl2Pcapng" -Recurse -Force -EA SilentlyContinue } # same thing, but with Convert-Etl2Pcapng.etl $test = Get-Item "HKCR:\Convert-Etl2Pcapng.etl" -EA SilentlyContinue if ($test) { Write-Verbose "Unregister-Etl2Pcapng: Removing Convert-Etl2Pcapng.etl HKCR app." Remove-Item "HKCR:\Convert-Etl2Pcapng.etl" -Recurse -Force -EA SilentlyContinue } # clean up legacy key $isOld = Get-ItemProperty -LiteralPath "HKCR:\.etl" -Name '(Default)' -EA SilentlyContinue if ($isOld.'(default)' -eq 'Convert-Etl2Pcapng') { # remove the default to .etl, but don't delete it Write-Verbose "Unregister-Etl2Pcapng: Cleanup .etl extension option." Set-ItemProperty -LiteralPath "HKCR:\.etl" -Name '(Default)' -Value "" -Force -EA SilentlyContinue } # clean up .etl stuff $test = Get-ItemProperty -LiteralPath "HKCR:\.etl" -Name 'Convert-Etl2Pcapng' -EA SilentlyContinue if ( $test ) { Remove-ItemProperty -LiteralPath "HKCR:\.etl" -Name 'Convert-Etl2Pcapng' -Force } # cleanup modern .etl location $test = Get-ItemProperty -LiteralPath "HKCR:\.etl\OpenWithProgids" -Name 'Convert-Etl2Pcapng.etl' -EA SilentlyContinue if ( $test ) { Remove-ItemProperty -LiteralPath "HKCR:\.etl\OpenWithProgids" -Name 'Convert-Etl2Pcapng.etl' -Force } # remove the SystemFileAssociation $rootPath = "HKCR:\SystemFileAssociations\.etl\shell\Convert-Etl2Pcapng" $testNew = Get-Item $rootPath -EA SilentlyContinue if ($testNew) { try { Remove-Item $rootPath -Recurse -Force -ErrorAction Stop } catch { Write-Error "Unregister-Etl2Pcapng: Failed to cleanup the SystemFileAssociations." } } # clean up the user folder Write-Verbose "Unregister-Etl2Pcapng: Cleaning up appDataPath directory." # read settings.xml $settings = Get-E2PSettings # for some reason the first pass doesn't return a E2PSettings class, so we force the issue if ($settings -isnot [E2PSettings]) { $settings = [E2PSettings]::new($settings) } try { # OneDrive safe delete process $isDirFnd = Get-Item "$($settings.appDataPath)\etl2pcapng" -EA SilentlyContinue # this should workaround the OneDrive bug if ($isDirFnd) { # first delete all the files $childs = Get-ChildItem -LiteralPath "$($isDirFnd.FullName)" -Recurse -Force -File foreach ($child in $childs) { $child.Delete() } # now get the directories $childs = Get-ChildItem -LiteralPath "$($isDirFnd.FullName)" -Recurse -Force foreach ($child in $childs) { $child.Delete() } # finally nuke the root dir $isDirFnd.Delete($true) } if (-NOT $NoClobber.IsPresent) { # removing the settings, too. It will regenerate again if needed. $null = Remove-Item "$($settings.appDataPath)\settings.xml" -Force -EA SilentlyContinue $null = Remove-Item "$($settings.appDataPath)\etl2pcapng.exe" -Force -EA SilentlyContinue } } catch { Write-Error "Unregister-Etl2Pcapng: Failed to cleanup the LocalAppData: $($settings.appDataPath)" } Write-Verbose "Unregister-Etl2Pcapng: Work complete!" } #end Unregister-Etl2Pcapng # FUNCTION : Convert-Etl2Pcapng # PURPOSE : Executes ETL2PCAPNG function Convert-Etl2Pcapng { <# .SYNOPSIS ndiscap tracing is the built-in packet capture tool used in Windows. ETL files cannot be read by third-party tools like Wireshark. Etl2pcapng converts ndiscap packet captures to a format readable by Wireshark. .DESCRIPTION This script converts ndiscap packets in an ETL into a Wireshark readable pcapng file. .PARAMETER Path The path to the ETL file or path containing the ETL file(s). When a container/directory is provided the script will search the partent directory for ETL files to convert. .PARAMETER Out The output path for the files. This parameter is optional. By default the script saves to the same directory the ETL file is located in. .PARAMETER Recurse Searches through child containers/directories for ETL files. Only valid when Path is a directory. .EXAMPLE Convert-Etl2Pcapng -Path C:\traces -Out D:\temp -Recurse Searches through C:\traces and all child directories for ETL files. The converted PCAPNG files will be saved to D:\temp. .EXAMPLE Convert-LBFO2Set C:\traces Converts all ETL files in C:\traces, but not any child directories, for ETLs and saves the PCAPNG files to the same directory (C:\traces). .NOTES Author: Microsoft Edge OS Networking Team and Microsoft CSS Please file issues on GitHub @ https://github.com/microsoft/Convert-Etl2Pcapng .LINK More projects : https://github.com/topics/msftnet Windows Networking Blog : https://blogs.technet.microsoft.com/networking/ #> [CmdletBinding(DefaultParameterSetName = 'LiteralPath')] param ( # Path accepts a literal string path. [parameter( Position = 0, ParameterSetName = 'LiteralPath', HelpMessage = 'Enter one or more filenames as a string', ValueFromPipeline = $false )] [string] $Path, # PSPath accepts a FileSystemInfo object from cmdlets like Get-Item and Get-ChildItem. Accepts pipeline input. [parameter( Position = 0, ParameterSetName = 'Path', HelpMessage = 'Enter one or more filenames as a string', ValueFromPipeline = $true )] [System.IO.FileSystemInfo] $PSPath, # Alternate output directory. [parameter( Position = 1, ParameterSetName = 'Path', ValueFromPipeline = $false )] [parameter( Position = 1, ParameterSetName = 'LiteralPath', ValueFromPipeline = $false )] [string] $Out = $null, # Search for ETL files in child directories. [parameter( Mandatory = $false, ParameterSetName = 'Path', ValueFromPipeline = $false )] [parameter( Mandatory = $false, ParameterSetName = 'LiteralPath', ValueFromPipeline = $false )] [switch] $Recurse, # Accepts EULA and skips prompt. [parameter( Mandatory = $false, ParameterSetName = 'Path', ValueFromPipeline = $false )] [parameter( Mandatory = $false, ParameterSetName = 'LiteralPath', ValueFromPipeline = $false )] [switch] $AcceptEULA, # Pauses the function at the end of execution. Used primarily for debugging with the context meny created by Register-Etl2Pcapng. [parameter( Mandatory = $false, ParameterSetName = 'Path', ValueFromPipeline = $false )] [parameter( Mandatory = $false, ParameterSetName = 'LiteralPath', ValueFromPipeline = $false )] [switch] $Pause ) Write-Verbose "Convert-Etl2Pcapng - Work! Work!" Write-Verbose "Convert-Etl2Pcapng - Using PowerShell Version $($PSVersionTable.PSVersion)" ### Validating paths and parameters ### # check the path param Write-Verbose "Convert-Etl2Pcapng - Validate Path." if ($PSPath -is [System.IO.FileSystemInfo]) { $isPathFnd = $PSPath } else { $isPathFnd = Get-Item "$Path" -EA SilentlyContinue } # if a dir/container, then look for ETL files if ($isPathFnd) { # is this a container/directory if ($isPathFnd.PSisContainer) { Write-Verbose "Convert-Etl2Pcapng - Searching for ETL files in $($isPathFnd.FullName)." # look for ETL files if ($Recurse) { Write-Verbose "Convert-Etl2Pcapng - Dir with child container recurse." [array]$etlFiles = Get-ChildItem $isPathFnd.FullName -Filter "*.etl" -Recurse -Force -ErrorAction SilentlyContinue } else { Write-Verbose "Convert-Etl2Pcapng - Dir with no child containers." [array]$etlFiles = Get-ChildItem $isPathFnd.FullName -Filter "*.etl" -Force -ErrorAction SilentlyContinue } } elseif ($isPathFnd.Extension -eq ".etl") { Write-Verbose "Convert-Etl2Pcapng - Single file." [array]$etlFiles = $isPathFnd } } # exit if no ETL file(s) found if (-NOT $etlFiles) { if ($PSPath -is [System.IO.FileSystemInfo]) { Write-Error "Convert-Etl2Pcapng - Failed to find a valid ETL file. Path: $($PSPath.FullName)" } else { Write-Error "Convert-Etl2Pcapng - Failed to find a valid ETL file. Path: $Path" } return $null } # make sure $Out is a valid location if ($Out) { Write-Verbose "Convert-Etl2Pcapng - Validate Out." if (-NOT (Test-Path $Out -IsValid)) { Write-Error "Convert-Etl2Pcapng - The Out path is an invalid path. Out: $Out" return $null } # create the dir if it's not there $isOutFnd = Get-Item "$Out" -EA SilentlyContinue if (-NOT $isOutFnd) { try { Write-Verbose "Convert-Etl2Pcapng - Creating output path $Out" New-Item "$Out" -ItemType Directory -Force -EA Stop | Out-Null } catch { Write-Error "Convert-Etl2Pcapng - Failed to create Out directory at $Out. Error: $($error[0].ToString())" } } } ### get the path to etl2pcapng.exe Write-Verbose "Convert-Etl2Pcapng - Getting for etl2pcapng location." try { if ($AcceptEULA.IsPresent) { [string]$e2pPath = Update-Etl2Pcapng -AcceptEULA } else { [string]$e2pPath = Update-Etl2Pcapng } } catch { return (Write-Error "Settings failure: $_" -EA Stop) } # validate etl2pcapng is actually there and strip out the parent dir if ($e2pPath) { Write-Verbose "Convert-Etl2Pcapng - Received etl2pcapng location: '$e2pPath'" # need this to trim a mysterious leading space when etl2pcapng is first downloaded and extracted $e2pPath = $e2pPath.Trim(" ") Write-Verbose "Convert-Etl2Pcapng - Validating etl2pcapng location: '$e2pPath'" # putting this in a loop due to OneDrive delay shinanigans $c = 0 do { Start-Sleep -m 250 $isE2PFnd = Get-Item "$e2pPath" -EA SilentlyContinue Write-Verbose "Convert-Etl2Pcapng - Validated e2p path: $($isE2PFnd.FullName)" $c++ } until ($isE2PFnd -or $c -ge 5) if ($isE2PFnd) { $e2pDir = $isE2PFnd.DirectoryName } else { Write-Error "Convert-Etl2Pcapng - Failed to locate etl2pcanpng.exe." return $null } } else { return $null } #### Finally do the conversion work #### Write-Verbose "Convert-Etl2Pcapng - Starting ETL to PCAPNG conversion(s)." Push-Location $e2pDir foreach ($file in $etlFiles) { # GI16 - Add pktmon support by saving the output from etl2pcapng.exe if ($Out) { $etlOutFile = "$Out\$($file.BaseName).pcapng" } else { $etlOutFile = "$($file.DirectoryName)\$($file.BaseName).pcapng" } # attempt the conversion Write-Verbose "Convert-Etl2Pcapng - Converting $($file.FullName) to $etlOutFile" $e2pOut = .\etl2pcapng.exe "$($file.FullName)" "$etlOutFile" 2>&1 Write-Verbose "Convert-Etl2Pcapng - etl2pcapng result:`n$($e2pOut | Out-String)`n" # GI16 - Convert with pktmon when etl2pcapng cannot if ($e2pOut -match "This file should be converted with pktmon") { Write-Verbose "Convert-Etl2Pcapng - Switching to pktmon to convert the ETL to pcapng." $e2pktOut = pktmon etl2pcap "$($file.FullName)" --out "$etlOutFile" 2>&1 Write-Verbose "Convert-Etl2Pcapng - pktmon etl2pcap result:`n$($e2pktOut | Out-String)`n" } } Pop-Location Write-Verbose "Convert-Etl2Pcapng - Work complete!" if ($Pause.IsPresent) { $null = Read-Host "Press Enter to continue..." } } #end Convert-Etl2Pcapng # FUNCTION : Update-Etl2Pcapng # PURPOSE : Gets the newest version of ETL2PCAPNG function Update-Etl2Pcapng { [CmdletBinding()] param( [switch]$Force, [switch]$AcceptEULA ) <# # Check for etl2pcapng updates only once a week # # The last time an update was checked for is located # in the module directory under settings.xml. # # The -Force param causes an etl2pcapng update check regardless of the last date checked. # #> Write-Verbose "Update-Etl2Pcapng - Starting" Write-Verbose "Update-Etl2Pcapng - OS architecture is $arch." # read settings.xml $settings = Get-E2PSettings # for some reason the first pass doesn't return a E2PSettings class, so we force the issue if ($settings -isnot [E2PSettings]) { $settings = [E2PSettings]::new($settings) } Write-Verbose "Update-Etl2Pcapng - Settings:`n`n$($settings | Format-List | Out-String)`n`nType: $($settings.GetType().ToString())" # store app data path in an easier to use var $here = $settings.appDataPath Write-Verbose "Update-Etl2Pcapng - Timestamps:`nCurrent date:`t$((Get-Date).Date)`nSettings date:`t$($settings.LastUpdate.Date)`n" # EULA prompt if ($AcceptEULA.IsPresent) { $settings.SetEulaStatus($true) Set-E2PSettings $settings } elseif ($settings.AcceptEULA -eq $false) { Write-Host @" Privacy Notice and End User License Agreement (EULA) This PowerShell module does not collect or upload data to Microsoft, third-parties, or Microsoft partners. Tracking and other statistical website data may be collected by PowerShellGallery.com when the module is downloaded, and by Github.com when the etl2pcapng.zip file is downloaded or updated by the module during cmdlet execution. By agreeing to the EULA you permit the Convert-Etl2Pcapng module to contact github.com to check, download, and extract etl2pcapng to this computer from github.com. "@ $c = 0 do { $answer = Read-Host "[A] Agree and continue, do not prompt in the future`n[Y] Agree once, prompt again (not recommended when using automation)`n[N] I do not agree, please terminate the script`nResponse" $c++ } until ($answer -eq 'a' -or $answer -eq 'y' -or $answer -eq 'n' -or $c -gt 3) switch ($answer) { 'a' { $settings.SetEulaStatus($true) Set-E2PSettings $settings break } 'y' { break } 'n' { return $null } default { return (Write-Error "Failed to get a valid user response to the EULA." -EA Stop)} } } # check for an update when -Force set or it's been 7 days since we last checked if ($Force -or ((Get-Date).Date.AddDays(-7) -gt $settings.LastUpdate.Date)) { Write-Verbose "Update-Etl2Pcapng - Checking for an update to etl2pcapng." Write-Verbose "Update-Etl2Pcapng - Getting etl2pcapng releases from GitGub." $repo = 'microsoft/etl2pcapng' try { $latest = Find-GitReleaseLatest $repo -EA Stop } catch { return (Write-Error "Failed to retrieve the latest release from repo: $repo" -EA Stop) } Write-Verbose "Update-Etl2Pcapng - The latest release is:`n`n$($latest | Format-Table | Out-String)" if ($latest.Version -gt $settings.CurrVersion) { Write-Verbose "Update-Etl2Pcapng - Cleaning up existing files." # remove the existing etl2pcapng copy(ies) Get-ChildItem "$here" -Filter "etl2pcapng.exe" | ForEach-Object { try { $e2pTmpPath = $_.FullName Write-Verbose "Update-Etl2Pcapng - Removing $e2pTmpPath" $null = Remove-Item $e2pTmpPath -Force -EA Stop } catch { Write-Warning "Failed to cleanup the old copy etl2pcapng at $e2pTmpPath`. Error: $_" } } Write-Verbose "Update-Etl2Pcapng - Downloading etl2pcapng" # grab the etl2pcapng tags page from GitHub try { # Version 1.10.0 and newer are an uncompressed amd64 binary $e2pPath = Get-WebFile -Uri $latest.URL -savePath "$here" -fileName "etl2pcapng.exe" -EA Stop } catch { return (Write-Error "Update-Etl2Pcapng - Cannot reach the etl2pcapng GitHub page: $_" -EA Stop) } # update the installed version [version]$version = $latest.Version Write-Verbose "Update-Etl2Pcapng - Updating version in settings to $($version.ToString())" $settings.SetCurrVersion($version) Write-Verbose "Update-Etl2Pcapng - here: $here" $settings.SetAppDataPath($here) $settings.SetE2PPath($e2pPath) } # update Settings.LastUpdate $updateTime = ([datetime]::Now).ToUniversalTime() Write-Verbose "Update-Etl2Pcapng - Updating last update check in settings to $($updateTime.ToString())" $settings.SetLastUpdate($updateTime) Write-Verbose "Update-Etl2Pcapng - Saving E2P settings changes." Write-Verbose "Update-Etl2Pcapng - Settings:`n$($settings.ToString())" Set-E2PSettings $settings } Write-Verbose "Update-Etl2Pcapng - Find etl2pcapng.exe" $isE2PFnd = Get-ChildItem "$here" -Filter "etl2pcapng.exe" -Recurse if ($isE2PFnd) { $fullPath = $isE2PFnd.FullName Write-Verbose "Update-Etl2Pcapng - etl2pcapng.exe found at: $fullPath" # 2025.05.001 - Update the E2PPath if that has changed if ($fullPath -ne $settings.GetE2PPath()) { Write-Verbose "Update-Etl2Pcapng - The etl2pcapng.exe path has changed. Updating settings." $settings.SetE2PPath($fullPath) # update settings in case anything changed Set-E2PSettings $settings } Write-Debug "Update-Etl2Pcapng - '$here'" Write-Verbose "Update-Etl2Pcapng - Work complete." return $fullPath } else { Write-Verbose "Update-Etl2Pcapng - Failed to find or download etl2pcapng.exe." Write-Verbose "Update-Etl2Pcapng - Work complete." return $null } } #end Update-Etl2Pcapng #endregion PUBLIC ################# ### ### ### PRIVATE ### ### ### ################# #region AUX ##### AUX functions that are not exported ##### # FUNCTION : Get-E2PSettings # PURPOSE : Finds and returns the module settings function Get-E2PSettings { if ($script:settings) { Write-Verbose "Get-E2PSettings - Looky, looky, I got settings!" return $script:settings } $rootPath = Find-E2PPath $setPath = "$rootPath\settings.xml" Write-Verbose "Get-E2PSettings - Using '$setPath' for the settings path." ## 2025.05.0001 - Upgrade scenario: older versions saved to USERPROFILE, new versions save to LOCALAPPDATA, migrate old to new. # look for settings in the current user module path and copy them if there $cuModPath = $env:PSModulePath -split ';' | Where-Object { $_ -match [regex]::Escape([System.Environment]::GetFolderPath("MyDocuments")) } $oldSettings = Get-ChildItem "$cuModPath\Convert-Etl2Pcapng\" -Filter "settings.xml" -EA SilentlyContinue if ($oldSettings) { # copy the files to LOCALAPPDATA Write-Verbose "Get-E2PSettings - Migrate existing files to LOCALAPPDATA." # list of settings files [array]$cpFiles = "settings*.xml", "etl2pcapng\etl2pcapng.exe" foreach ($file in $cpFiles) { Write-Verbose "Get-E2PSettings - Copying: $file" $null = Move-Item "$cuModPath\Convert-Etl2Pcapng\$file" -Destination "$rootPath" -Force } Write-Verbose "Get-E2PSettings - Migration complete." } ## # look for an existing settings file $isADP = Get-Item "$setPath" -ErrorAction SilentlyContinue # read the file if it exists if ($isADP) { Write-Verbose "Get-E2PSettings - Settings file found. Getting settings from file." # read the settings file try { $settings = [E2PSettings]::New((Import-Clixml "$setPath")) } catch { return (Write-Error "Failed to import settings: $_" -EA Stop) } ## 2025.05.0001 - Update appDataPath if the path changed due to an update. if ($settings.appDataPath -ne $rootPath) { Write-Verbose "Get-E2PSettings - Migrate appDataPath to LOCALAPPDATA." $settings.SetAppDataPath($rootPath) # E2PPath will update other places. # save the settings change $null = Set-E2PSettings $settings } ## # return the settings Write-Verbose "Get-E2PSettings - Work complete!" $script:settings = $settings return $script:settings # create the file if it does not exist } else { # create the etl2pcapng dir Write-Verbose "Get-E2PSettings - Settings file not found. Using defaults." $settings = New-E2PSetting # write the settings file Write-Verbose "Get-E2PSettings - Writing settings." Set-E2PSettings $settings # return settings Write-Verbose "Get-E2PSettings - Work complete!" $script:settings = [E2PSettings]::new($settings) return $script:settings } Write-Verbose "Get-E2PSettings - Something unexpected went wrong and no settings were returned." return $null } #end Get-E2PSettings # FUNCTION : Set-E2PSettings # PURPOSE : Updates E2P settings function Set-E2PSettings { [CmdletBinding()] param ( [PSCustomObject]$settings ) Write-Verbose "Set-E2PSettings - Begin!" Write-Verbose "Set-E2PSettings - Settings:`n$($settings.ToString())" $isAPDFnd = Get-Item "$($settings.appDataPath)" -EA SilentlyContinue try { if (-NOT $isAPDFnd) { Write-Verbose "Set-E2PSettings - Creating appDatPath: $($settings.appDataPath)" New-Item -Path "$($settings.appDataPath)" -ItemType Directory -Force -EA Stop | Out-Null } Write-Verbose "Set-E2PSettings - Exporting settings to $($settings.appDataPath)\settings.xml" $settings | Export-Clixml -Path "$($settings.appDataPath)\settings.xml" -Depth 10 -Force -EA Stop # 2025.05.01 - Update the module wide settings variable if ($script:settings) { $script:settings = $settings } } catch { return (Write-Error "Failed to write settings.xml to $($settings.appDataPath): $_" -EA Stop) } Write-Verbose "Set-E2PSettings - End!" return $null } function New-E2PSetting { [string]$here = Find-E2PPath # create default settings $defSettings = [E2PSettings]::New($here) return $defSettings } function Find-E2PPath { if ( -NOT [string]::IsNullOrEmpty($script:here) ) { Write-Verbose "Find-E2PPath - We've already got one, it's very nice: $here" return $script:here } Write-Verbose "Find-E2PPath - Searching for installed module." # make sure the running module $mods = Get-Module Convert-Etl2Pcapng -EA SilentlyContinue # fall back to listed modules if none are running if (-NOT $mods) { $mods = Get-Module -ListAvailable Convert-Etl2Pcapng -EA SilentlyContinue } if ($mods) { # GI15: The CurrentUser PSModulePath is in Documents\PowerShell or OneDrive\Documents\PowerShell # The original code did not take this into account. # # original code: $here = $env:PSModulePath -split ';' | Where-Object { $_ -match "$([regex]::Escape("$env:USERPROFILE"))" } # # New process: Always use LocalAppData to avoid redirects of the Documents dir. # # The SYSTEM and Local Service accounts will use C:\WINDOWS\system32\config\systemprofile\AppData\Local for $env:LocalAppData, # so this should work with automated processes that might be using this script. Write-Verbose "Find-E2PPath - Had to fall back to legacy %LOCALAPPDATA% path." # we avoid using admin module paths since I can't dynamnically add files there $here = "$env:LOCALAPPDATA\Convert-Etl2Pcapng" # now that this is decided, add it to the script variable scope to speed things up in the future $script:here = $here } else { return (Write-Error "This cannot be run outside of the Convert-ETL2PCAPNG module. Please install the Convert-ETL2PCAPNG module first:`n`nInstall-Module Convert-ELT2PCAPNG." -EA Stop) } if (-NOT $here) { return (Write-Error "Failed to find a current user module path." -EA Stop) } # create the directory if it is missing $dirFnd = Get-ChildItem $env:LOCALAPPDATA -Filter 'Convert-Etl2Pcapng' -Directory if (-NOT $dirFnd) { Write-Verbose "Find-E2PPath - Creating: $here" $null = mkdir "$here" -Force -EA SilentlyContinue } Write-Verbose "Find-E2PPath - Returning: $here" return $here } # FUNCTION: Get-WebFile # PURPOSE: Downloads a file from the Internet. Returns the full path to the download. function Get-WebFile { param ( [string]$URI, [string]$Path, [string]$FileName ) Write-Debug "Get-WebFile - Start." # validate path if ( -NOT (Test-Path "$Path" -IsValid) ) { return (Write-Error "The save path, $Path, is not valid. Error: $_" -EA Stop) } # create the path if missing if ( -NOT (Get-Item "$Path" -EA SilentlyContinue) ) { try { $null = mkdir "$Path" -Force -EA Stop } catch { return (Write-Error "The save path, $Path, does not exist and cannot be created. Error: $_" -EA Stop) } } # create the full path $OutFile = "$Path\$fileName" # use curl if it is found in the path # options are iwr (Invoke-WebRequest (default)), bits (Start-BitsTransfer), and curl (preferred when found) $dlMethods = "iwr", "curl", "bits" $dlMethod = "iwr" # switch to curl when found $curlFnd = Get-Command "curl.exe" -EA SilentlyContinue if ($curlFnd) { $dlMethod = "curl" } Write-Verbose "Get-WebFile - Attempting download of $URI to $OutFile" # did the download work? $dlWorked = $false # methods tried # initialize with curl because if curl is found then we're using it, if it's not found then we shouldn't try it $tried = @("curl") # loop through do { switch ($dlMethod) { # tracks whether "curl" { Write-Verbose "Get-WebFile - Download with curl." Push-Location "$Path" # download with curl # -L = download location # -o = output file # -s = Silent curl.exe -L $URI -o $OutFile -s Pop-Location } "iwr" { Write-Verbose "Get-WebFile - Download with Invoke-WebRequest." # make sure we don't try to use an insecure SSL/TLS protocol when downloading files Write-Debug "Get-WebFile - Disabling unsupported SSL/TLS protocls." [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12, [System.Net.SecurityProtocolType]::Tls13 # download silently with iwr $oldProg = $global:ProgressPreference $Global:ProgressPreference = "SilentlyContinue" $null = Invoke-WebRequest -Uri $URI -OutFile "$OutFile" -MaximumRedirection 5 -PassThru $Global:ProgressPreference = $oldProg } "bits" { Write-Verbose "Get-WebFile - Download with Start-BitsTransfer." # download silently with iwr $oldProg = $global:ProgressPreference $Global:ProgressPreference = "SilentlyContinue" $null = Start-BitsTransfer -Source $URI -Destination "$OutFile" $Global:ProgressPreference = $oldProg } Default { return (Write-Error "An unknown download method was selected. This should not happen. dlMethod: $_" -EA Stop) } } # is there a file, any file, then consider this a success $dlFnd = Get-Item "$OutFile" -EA SilentlyContinue if ( -NOT $dlFnd ) { # change download method and try again Write-Verbose "Failed to download using $dlMethod." if ($tried.Count -lt $dlMethods.Count) { if ($dlMethod -notin $tried) { $tried += $dlMethod Write-Verbose "Get-WebFile - Added $dlMethod to tried: $($tried -join ', ')" } :dl foreach ($dl in $dlMethods) { if ($dl -notin $tried) { Write-Verbose "Get-WebFile - Switching to $dl method." $dlMethod = $dl $tried += $dl break dl } } } else { return (Write-Error "The download has failed!" -EA Stop) } } else { # exit the loop $dlWorked = $true } } until ($dlWorked) Write-Verbose "Get-WebFile - File downloaded to $OutFile." #Add-Log "Downloaded successfully to: $output" Write-Debug "Get-WebFile - Returning: $OutFile" Write-Debug "Get-WebFile - End." return $OutFile } # FUNCTION: Find-GitReleaseLatest # PURPOSE: Calls Github API to retrieve details about the latest release. Returns a PSCustomObject with repro, version (tag_name), and download URL. function Find-GitReleaseLatest { [CmdletBinding()] param( [string]$repo ) Write-Verbose "Find-GitReleaseLatest - Begin" $baseApiUri = "https://api.github.com/repos/$($repo)/releases/latest" # make sure we don't try to use an insecure SSL/TLS protocol when downloading files $secureProtocols = @() $insecureProtocols = @( [System.Net.SecurityProtocolType]::SystemDefault, [System.Net.SecurityProtocolType]::Ssl3, [System.Net.SecurityProtocolType]::Tls, [System.Net.SecurityProtocolType]::Tls11) foreach ($protocol in [System.Enum]::GetValues([System.Net.SecurityProtocolType])) { if ($insecureProtocols -notcontains $protocol) { $secureProtocols += $protocol } } [System.Net.ServicePointManager]::SecurityProtocol = $secureProtocols # get the available releases Write-Verbose "Find-GitReleaseLatest - Processing repro: $repo" Write-Verbose "Find-GitReleaseLatest - Making Github API call to: $baseApiUrl" try { if ($pshost.Version.Major -le 5) { $rawReleases = Invoke-WebRequest $baseApiUri -UseBasicParsing -EA Stop } elseif ($pshost.Version.Major -ge 6) { $rawReleases = Invoke-WebRequest $baseApiUri -EA Stop } else { return (Write-Error "Unsupported version of PowerShell...?" -EA Stop) } } catch { return (Write-Error "Could not get GitHub releases. Error: $_" -EA Stop) } Write-Verbose "Find-GitReleaseLatest - Processing results." try { [version]$version = ($rawReleases.Content | ConvertFrom-Json).tag_name.Trim("v") } catch { $version = ($rawReleases.Content | ConvertFrom-Json).tag_name } Write-Verbose "Find-GitReleaseLatest - Found version: $version" $dlURI = ($rawReleases.Content | ConvertFrom-Json).Assets.browser_download_url Write-Verbose "Find-GitReleaseLatest - Found download URL: $dlURI" Write-Verbose "Find-GitReleaseLatest - End" return ([PSCustomObject]@{ Repo = $repo Version = $version URL = $dlURI }) } #end Find-GitReleaseLatest # FUNCTION: Find-E2PSoftware # PURPOSE: Gets a list of all installed software from the registry with optional filter on the DisplayName. function Find-E2PSoftware { [CmdletBinding()] param ($displayFilter = $null) Write-Verbose "Find-E2PSoftware - Begin" $apps = @() [string[]]$regPaths = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall", "HKLM:\SOFTWARE\Wow6432node\Microsoft\Windows\CurrentVersion\Uninstall" foreach ($regPath in $regPaths) { Write-Verbose "Find-E2PSoftware: Checking Path: $regPath" try { $reg = Get-Item $regPath -ErrorAction Stop } catch { Write-Debug "Find-E2PSoftware: Could not find the path: $_ `n`n $reg " continue } # change the EAP to stop to force the try to fail if there is an error $ErrorActionPreference = "SilentlyContinue" # get all the child keys [array]$regkeys = Get-ChildItem $regPath #echo "$($regKeys.PSChildName | Out-String)" foreach ($key in $regkeys) { Write-Verbose "Find-E2PSoftware: $($key.PSChildName)" if ($displayFilter) { Write-Verbose "Find-E2PSoftware: Filter $((Get-ItemProperty -Path $key.PsPath -Name DisplayName).DisplayName) match $displayFilter" if ( "$((Get-ItemProperty -Path $key.PsPath -Name DisplayName).DisplayName)" -match $displayFilter) { # create the PsCustomObject that stores software details $tmpObj = [pscustomobject]@{ Name = $key.PSChildName } # loop through all the properties and add them to the object $key.Property | ForEach-Object { $tmpObj | Add-Member -Name $_ -MemberType NoteProperty -Value "$((Get-ItemProperty -Path $key.PsPath -Name $_)."$_")" } # add the software to the apps array $apps += $tmpObj Remove-Variable tmpObj } } else { $tmpObj = [pscustomobject]@{ Name = $key.PSChildName } $key.Property | ForEach-Object { $tmpObj | Add-Member -Name $_ -MemberType NoteProperty -Value "$((Get-ItemProperty -Path $key.PsPath -Name $_)."$_")" } # add the software to the apps array $apps += $tmpObj Remove-Variable tmpObj } } } Write-Verbose "Find-E2PSoftware: Work complete!" return $apps } #end Find-E2PSoftware function New-RegKey { [CmdletBinding()] param( [string]$path, [string]$type, $value ) Write-Verbose "New-RegKey: Starting" # make sure the PSDrive to HKCR is created if (-NOT (Get-PSDrive -Name HKCR -EA SilentlyContinue)) { New-PSDrive -Name HKCR -PSProvider Registry -Root HKEY_CLASSES_ROOT -Scope Local | Out-Null } # do the reg work try { Write-Verbose "New-RegKey: Creating key $path" if ($type -eq "Directory") { New-Item "$path" -ItemType $type -Force -EA SilentlyContinue | Out-Null } else { Write-Verbose "New-RegKey: Setting property on $path to $value" Set-ItemProperty -LiteralPath $path -Name '(Default)' -Value $value -Force -EA SilentlyContinue | Out-Null } } catch { Write-Error "New-RegKey: Failed to create $path." return $false } Write-Verbose "New-RegKey: Work complete!" return $true } ### CLASSES ### #region CLASSES class E2PSettings { [datetime]$LastUpdate [version]$CurrVersion [string]$appDataPath [string]$E2PPath [bool]$AcceptEULA [version]$CurrE2PVersion #region construtors E2PSettings() { $this.LastUpdate = [datetime]::FromFileTimeUtc(0) $this.CurrVersion = [version]::new() $this.appDataPath = $null $this.E2PPath = $null $this.AcceptEULA = $false $this.CurrE2PVersion = $null } E2PSettings([string]$path) { $this.LastUpdate = [datetime]::FromFileTimeUtc(0) $this.CurrVersion = [version]::new() $this.appDataPath = $path $this.E2PPath = $null $this.AcceptEULA = $false $this.CurrE2PVersion = $null } E2PSettings([PSCustomObject]$set) { $this.LastUpdate = $set.LastUpdate $this.CurrVersion = $set.CurrVersion $this.appDataPath = $set.appDataPath $this.E2PPath = $set.E2PPath $this.AcceptEULA = $set.AcceptEULA if ( -NOT $set.CurrE2PVersion) { $this.CurrE2PVersion = $script:CurrentE2PVersion } else { $this.CurrE2PVersion = $set.CurrE2PVersion } } #endregion construtors #region getters [datetime]GetLastUpdate() { return ($this.LastUpdate) } [version]GetCurrVersion() { return ($this.CurrVersion) } [string]GetAppDataPath() { return ($this.appDataPath) } [string]GetE2PPath() { return ($this.E2PPath) } [bool]GetEulaStatus() { return ($this.AcceptEULA) } [version]GetCurrE2PVersion() { return ($this.CurrE2PVersion) } #endregion getters #region setters SetLastUpdate([datetime]$LastUpdate) { if ($null -ne $LastUpdate) { $this.LastUpdate = $LastUpdate } } SetCurrVersion([version]$CurrVersion) { if ($null -ne $CurrVersion) { $this.CurrVersion = $CurrVersion } } SetAppDataPath([string]$appDataPath) { if ($null -ne $appDataPath) { $this.appDataPath = $appDataPath } } SetE2PPath([string]$E2PPath) { if ($null -ne $E2PPath) { $this.E2PPath = $E2PPath } } SetEulaStatus([bool]$eulaStatus) { $this.AcceptEULA = $eulaStatus } SetCurrE2PVersion([version]$ver) { if ($null -ne $ver) { $this.CurrE2PVersion = $ver } } #endregion setters #region methods [string]ToString() { return (@" LastUpdate : $($this.appDataPath) CurrVersion : $($this.CurrVersion) appDataPath : $($this.appDataPath) E2PPath : $($this.E2PPath) AcceptEULA : $($this.AcceptEULA) CurrE2PVersion : $($this.CurrE2PVersion) "@) } # by returning an ErrorRecord the Save method can create a terminating error in a caller using a try-catch [System.Management.Automation.ErrorRecord] Save($Filename) { # convert the filename to a string if we get a filesystem object from something like Get-Item if ($Filename -is [System.IO.FileSystemInfo]) { $Filename = $Filename.Fullname } # make sure the file is valid if (-NOT (Test-Path "$Filename" -IsValid)) { # return a terminating error return (Write-Error "[E2PSettings].Save - The filename is invalid: $Filename" -EA Stop) } # no check if the file exists. This function explicitly overwrites the existing content try { $this | Export-Clixml -Path "$Filename" -Depth 20 -Encoding utf8 -Force -EA Stop } catch { # return a terminating error return (Write-Error "[E2PSettings].Save - Could not save the settings file: $_" -EA Stop) } # return a nonterminating null return $null } #endregion methods } #endregion CLASSES #endregion AUX #region TYPE ACCELERATORS $ExportableTypes =@( [E2PSettings] ) # Get the internal TypeAccelerators class to use its static methods. $TypeAcceleratorsClass = [psobject].Assembly.GetType( 'System.Management.Automation.TypeAccelerators' ) # Ensure none of the types would clobber an existing type accelerator. # If a type accelerator with the same name exists, throw an exception. $ExistingTypeAccelerators = $TypeAcceleratorsClass::Get foreach ($Type in $ExportableTypes) { if ($Type.FullName -in $ExistingTypeAccelerators.Keys) { # throw a message to the verbose stream Write-Verbose @" Unable to register type accelerator[$($Type.FullName)]. The Accelerator already exists. "@ } else { # import the class $TypeAcceleratorsClass::Add($Type.FullName, $Type) } } # Remove type accelerators when the module is removed. $MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = { foreach($Type in $ExportableTypes) { $TypeAcceleratorsClass::Remove($Type.FullName) } }.GetNewClosure() #endregion # the list of functions the module will export. Export-ModuleMember -Function Register-Etl2Pcapng Export-ModuleMember -Function Unregister-Etl2Pcapng Export-ModuleMember -Function Convert-Etl2Pcapng Export-ModuleMember -Function Update-Etl2Pcapng