Public/Extract-SCOMMSI.ps1
|
Function Extract-SCOMMSI { <# .SYNOPSIS Extracts sealed Management Pack files (.mp, .mpb) from SCOM MSI installer packages. .DESCRIPTION Extract-SCOMMSI processes one or more .msi installer packages and extracts any sealed Management Pack files (.mp, .mpb) contained within them. Extraction uses msiexec admin install (msiexec /a) to reliably unpack the MSI contents. The function supports two input modes: - Path mode (default): Specify paths to .msi files or directories containing .msi files. - Pipeline mode: Pipe FileInfo objects (e.g., from Get-ChildItem) for selective processing. Use -PassThru to emit FileInfo objects for each extracted .mp/.mpb file, enabling pipeline composition with Unseal-SCOMMP. .PARAMETER Path One or more paths to .msi files or directories containing .msi files. Directories are searched recursively for .msi files. .PARAMETER InputObject A System.IO.FileInfo object received from the pipeline. Non-.msi files are skipped with a warning. .PARAMETER OutputDir Path to the output directory where extracted .mp/.mpb files will be written. Created automatically if it does not exist. .PARAMETER PerPackage Creates a subfolder per MSI file (named after the MSI basename) in the output directory. If a subfolder with that name already exists, a numeric suffix is appended (2, 3, 4...). .PARAMETER Force Overwrites existing files in the output directory. Without this switch, existing files with the same name are skipped. .PARAMETER PassThru Emits a System.IO.FileInfo object for each extracted .mp/.mpb file. This enables piping directly to Unseal-SCOMMP for end-to-end extraction and unsealing. .INPUTS System.IO.FileInfo — .msi file objects from Get-ChildItem. .OUTPUTS System.IO.FileInfo — when -PassThru is specified; one object per extracted .mp/.mpb file. None — when -PassThru is not specified. .EXAMPLE Extract-SCOMMSI -Path 'C:\Downloads\SCOM_MPs.msi' -OutputDir 'C:\Extracted' Extracts all .mp and .mpb files from a single MSI package. .EXAMPLE Extract-SCOMMSI -Path 'C:\Downloads\MSIs' -OutputDir 'C:\Extracted' -PerPackage Extracts from all MSI files in the directory, creating a subfolder per MSI. .EXAMPLE Get-ChildItem 'C:\Downloads' -Filter '*.msi' | Extract-SCOMMSI -OutputDir 'C:\Extracted' Pipes MSI files from a directory into Extract-SCOMMSI. .EXAMPLE Extract-SCOMMSI -Path 'C:\Downloads\*.msi' -OutputDir 'C:\Extracted' -PassThru Extracts and emits FileInfo objects for each extracted MP file. .EXAMPLE Extract-SCOMMSI -Path 'C:\Downloads' -OutputDir 'C:\Extracted' -PassThru | Unseal-SCOMMP -OutputDir 'C:\Unsealed' End-to-end pipeline: extracts .mp/.mpb from MSI packages then pipes them to Unseal-SCOMMP for unsealing to XML. .EXAMPLE Extract-SCOMMSI -Path 'C:\MSI1.msi','C:\MSI2.msi' -OutputDir 'C:\Out' -PerPackage -Force Processes multiple MSI files, creates per-package subfolders, and overwrites existing output. .NOTES Author: Tyson Paul Blog: https://monitoringguys.com/ Requires: msiexec.exe (included with Windows) History: 2025.07.14 - Initial. Extracted from Export-SCOMManagementPackHelper as a focused MSI extraction function. 2026.04.10 - Added temp-copy for long input paths (>200 chars) to avoid msiexec 1603 failures. .LINK Unseal-SCOMMP Get-SCOMMPFileInfo #> [CmdletBinding(DefaultParameterSetName = 'ByPath')] param( [Parameter(Mandatory, Position = 0, ParameterSetName = 'ByPath')] [ValidateScript({ Test-Path $_ })] [string[]]$Path, [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'ByPipeline')] [System.IO.FileInfo]$InputObject, [Parameter(Mandatory, Position = 1)] [string]$OutputDir, [Parameter()] [switch]$PerPackage, [Parameter()] [switch]$Force, [Parameter()] [switch]$PassThru ) Begin { #region Internal helper: Get unique subfolder name function Get-UniqueSubfolder { param([string]$ParentDir, [string]$BaseName) $candidate = Join-Path $ParentDir $BaseName if (-not (Test-Path $candidate)) { return $candidate } $counter = 2 while (Test-Path (Join-Path $ParentDir "$BaseName$counter")) { $counter++ } return (Join-Path $ParentDir "$BaseName$counter") } #endregion # Create output directory if needed if (-not (Test-Path $OutputDir)) { Write-Verbose "Creating output directory: $OutputDir" New-Item -Path $OutputDir -ItemType Directory -Force | Out-Null } $allFailed = @() $processedCount = 0 # Collect MSI files for Path mode $msiFiles = @() if ($PSCmdlet.ParameterSetName -eq 'ByPath') { foreach ($p in $Path) { $item = Get-Item $p if ($item.PSIsContainer) { $msiFiles += @(Get-ChildItem $item.FullName -Filter '*.msi' -Recurse -File -ErrorAction SilentlyContinue) } elseif ($item.Extension -eq '.msi') { $msiFiles += $item } else { Write-Warning "Skipping '$($item.Name)' - not an .msi file or directory." } } if ($msiFiles.Count -eq 0) { Write-Warning "No .msi files found in the specified path(s)." return } Write-Verbose "Found $($msiFiles.Count) .msi file(s) to process." } } Process { if ($PSCmdlet.ParameterSetName -eq 'ByPipeline') { if ($InputObject.Extension -ne '.msi') { Write-Warning "Skipping '$($InputObject.Name)' - not an .msi file." return } $msiFiles = @($InputObject) } foreach ($msiFile in $msiFiles) { $processedCount++ $tempExtractDir = $null $tempMsiCopy = $null try { Write-Verbose "Processing MSI: $($msiFile.FullName)" # Copy MSI to temp if input path is long (msiexec fails >~200 chars) $msiPath = $msiFile.FullName if ($msiPath.Length -gt 200) { $tempMsiCopy = Join-Path $env:TEMP $msiFile.Name Write-Verbose " Input path is $($msiPath.Length) chars — copying to temp to avoid msiexec path limits." Copy-Item -LiteralPath $msiPath -Destination $tempMsiCopy -Force $msiPath = $tempMsiCopy } # Determine output directory for this MSI if ($PerPackage) { $msiBaseName = [System.IO.Path]::GetFileNameWithoutExtension($msiFile.Name) $msiOutDir = Get-UniqueSubfolder -ParentDir $OutputDir -BaseName $msiBaseName } else { $msiOutDir = $OutputDir } New-Item -Path $msiOutDir -ItemType Directory -Force -ErrorAction SilentlyContinue | Out-Null # Create temp working directory for extraction $tempExtractDir = Join-Path $msiOutDir "_msi_extract_temp_$([guid]::NewGuid().ToString('N').Substring(0,8))" New-Item -Path $tempExtractDir -ItemType Directory -Force | Out-Null # Extract MSI contents using msiexec admin install Write-Verbose " Extracting MSI contents via msiexec /a ..." $msiexecArgs = "/a `"$msiPath`" /qn TARGETDIR=`"$tempExtractDir`"" $proc = Start-Process msiexec.exe -ArgumentList $msiexecArgs -Wait -PassThru -NoNewWindow if ($proc.ExitCode -ne 0) { throw "msiexec admin install failed with exit code $($proc.ExitCode)" } # Find all MP/MPB files in extracted content $mpFiles = @(Get-ChildItem $tempExtractDir -Include '*.mp','*.mpb' -Recurse -File -ErrorAction SilentlyContinue) Write-Verbose " Found $($mpFiles.Count) Management Pack file(s) in MSI." if ($mpFiles.Count -eq 0) { Write-Warning "No .mp or .mpb files found in MSI '$($msiFile.Name)'." $allFailed += $msiFile.Name continue } # Copy extracted MP/MPB files to output foreach ($mpf in $mpFiles) { $destFile = Join-Path $msiOutDir $mpf.Name if ((Test-Path -LiteralPath $destFile) -and -not $Force) { Write-Verbose " Skipping '$($mpf.Name)' - exists. Use -Force to overwrite." } else { Copy-Item -LiteralPath $mpf.FullName -Destination $destFile -Force Write-Verbose " -> Extracted: $destFile" } if ($PassThru) { Get-Item -LiteralPath $destFile } } } catch { Write-Warning "Failed to process MSI '$($msiFile.Name)': $_" $allFailed += $msiFile.Name } finally { # Clean up temp extraction directory if ($tempExtractDir -and (Test-Path $tempExtractDir)) { Remove-Item -Path $tempExtractDir -Recurse -Force -ErrorAction SilentlyContinue } # Clean up temp MSI copy if ($tempMsiCopy -and (Test-Path $tempMsiCopy)) { Remove-Item -Path $tempMsiCopy -Force -ErrorAction SilentlyContinue } } } # Reset for next pipeline iteration if ($PSCmdlet.ParameterSetName -eq 'ByPipeline') { $msiFiles = @() } } End { $successCount = $processedCount - $allFailed.Count if ($processedCount -gt 0) { Write-Verbose "Extraction complete. $successCount of $processedCount MSI(s) succeeded." } if ($allFailed.Count -gt 0) { Write-Warning "The following $($allFailed.Count) MSI(s) failed to process:" $allFailed | ForEach-Object { Write-Warning " - $_" } } } } |