Public/Unseal-SCOMMP.ps1

Function Unseal-SCOMMP {
  <#
      .SYNOPSIS
      Unseals SCOM Management Pack files (.mp, .mpb) to XML and extracts embedded resources.
 
      .DESCRIPTION
      Unseal-SCOMMP processes sealed Management Pack files (.mp) and Management Pack Bundles (.mpb),
      converting them to unsealed XML format and extracting any embedded resources (DLLs, images,
      SQL scripts, etc.). Each input file is placed into a version-coded subfolder in the output
      directory named FileName(Version).
 
      The function supports two input modes:
      - Directory mode (default): Specify -InputDir to process all .mp/.mpb files in a directory.
      - Pipeline mode: Pipe FileInfo objects (e.g., from Get-ChildItem) for selective processing.
 
      SDK assemblies are auto-discovered from the GAC, common SCOM install paths, registry, or an
      explicit -SdkPath parameter.
 
      .PARAMETER InputDir
      Path to the directory containing sealed .mp and/or .mpb files. Searches recursively.
      Alias: inDir (for backward compatibility with the original Unseal-SCOMMP).
 
      .PARAMETER InputObject
      A System.IO.FileInfo object received from the pipeline. Must have a .mp or .mpb extension;
      other file types are skipped with a warning.
 
      .PARAMETER OutputDir
      Path to the output directory where unsealed XML and resources will be written. Created
      automatically if it does not exist.
      Alias: outDir (for backward compatibility with the original Unseal-SCOMMP).
 
      .PARAMETER SdkPath
      Explicit path to the directory containing the SCOM SDK DLLs
      (Microsoft.EnterpriseManagement.Core.dll and Microsoft.EnterpriseManagement.Packaging.dll).
      If not specified, the function attempts auto-discovery via GAC, $env:SCOM_SDK_PATH,
      common SCOM install paths, and the registry.
 
      .PARAMETER Force
      When specified, overwrites existing output folders. Without this switch, files whose
      version-coded output folder already exists are skipped.
 
      .INPUTS
      System.IO.FileInfo — from Get-ChildItem or Extract-SCOMMSI -PassThru.
 
      .OUTPUTS
      None. Writes unsealed XML files and resources to the output directory.
 
      .EXAMPLE
      Unseal-SCOMMP -inDir C:\MyMPs -outDir C:\MyMPs\UnsealedMPs
 
      Unseals all .mp and .mpb files in C:\MyMPs using the legacy parameter names.
 
      .EXAMPLE
      Unseal-SCOMMP -InputDir 'C:\Program Files (x86)\System Center Management Packs' -OutputDir 'C:\Temp\Unsealed'
 
      Unseals all management packs from a System Center Management Pack directory.
 
      .EXAMPLE
      Get-ChildItem 'C:\MPs' -Filter '*.mpb' | Unseal-SCOMMP -OutputDir 'C:\Unsealed'
 
      Pipes only .mpb files into Unseal-SCOMMP for selective processing.
 
      .EXAMPLE
      Unseal-SCOMMP -InputDir 'C:\MPs\Sealed' -OutputDir 'C:\MPs\Unsealed' -Force
 
      Re-runs unsealing and overwrites any previously exported folders.
 
      .EXAMPLE
      Unseal-SCOMMP -InputDir 'C:\MPs' -OutputDir 'C:\Out' -SdkPath 'D:\SCOM\SDK'
 
      Uses SCOM SDK DLLs from a custom path when they are not in the GAC or standard locations.
 
      .EXAMPLE
      Extract-SCOMMSI -Path 'C:\Downloads\*.msi' -OutputDir 'C:\Extracted' -PassThru | Unseal-SCOMMP -OutputDir 'C:\Unsealed'
 
      End-to-end pipeline: extracts .mp/.mpb from MSI packages then unseals them to XML.
 
      .NOTES
      Author: Tyson Paul
      Blog: https://monitoringguys.com/
      Requires: SCOM SDK DLLs (Microsoft.EnterpriseManagement.Core, Microsoft.EnterpriseManagement.Packaging)
      History:
      2018.05.25 - Initial.
      2019.05.01 - Fixed issue where output path would display extra backslash.
      2025.07.14 - Major upgrade: CmdletBinding, pipeline input, SDK auto-discovery, -SdkPath,
                  -Force, per-file error handling, Write-Host replaced with Write-Verbose.
 
      .LINK
      Extract-SCOMMSI
      Get-SCOMMPFileInfo
      Get-SCOMClassInfo
  #>


  [CmdletBinding(DefaultParameterSetName = 'ByDirectory')]
  param(
    [Parameter(Mandatory, Position = 0, ParameterSetName = 'ByDirectory')]
    [ValidateScript({ Test-Path $_ -PathType Container })]
    [Alias('inDir')]
    [string]$InputDir,

    [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'ByPipeline')]
    [System.IO.FileInfo]$InputObject,

    [Parameter(Mandatory, Position = 1)]
    [Alias('outDir')]
    [string]$OutputDir,

    [Parameter()]
    [string]$SdkPath,

    [Parameter()]
    [switch]$Force
  )

  Begin {
    #region Internal helper: Load SDK DLLs
    function Initialize-SdkAssemblies {
      param([string]$ExplicitPath)

      $requiredDlls = @(
        'Microsoft.EnterpriseManagement.Core',
        'Microsoft.EnterpriseManagement.Packaging'
      )

      # Attempt 1: GAC via LoadWithPartialName
      $allLoaded = $true
      foreach ($dll in $requiredDlls) {
        $asm = [Reflection.Assembly]::LoadWithPartialName($dll)
        if ($null -eq $asm) {
          $allLoaded = $false
          Write-Verbose "SDK DLL '$dll' not found in GAC."
          break
        }
        else {
          Write-Verbose "Loaded '$dll' from GAC: $($asm.Location)"
        }
      }
      if ($allLoaded) { return $true }

      # Attempt 2: Search explicit paths
      $searchPaths = @()
      if ($ExplicitPath) { $searchPaths += $ExplicitPath }
      if ($env:SCOM_SDK_PATH) { $searchPaths += $env:SCOM_SDK_PATH }
      # SCOM 2019+ paths
      $searchPaths += "${env:ProgramFiles}\Microsoft System Center\Operations Manager\Server\SDK Binaries"
      $searchPaths += "${env:ProgramFiles}\Microsoft System Center\Operations Manager\Console\SDK Binaries"
      # SCOM 2016 paths
      $searchPaths += "${env:ProgramFiles}\Microsoft System Center Operations Manager\Server\SDK Binaries"
      $searchPaths += "${env:ProgramFiles}\Microsoft System Center Operations Manager\Console\SDK Binaries"

      # Attempt 3: Registry probe
      $regKey = 'HKLM:\SOFTWARE\Microsoft\System Center Operations Manager\12\Setup'
      if (Test-Path $regKey) {
        $installDir = (Get-ItemProperty -Path $regKey -ErrorAction SilentlyContinue).InstallDirectory
        if ($installDir) {
          $searchPaths += (Join-Path $installDir 'SDK Binaries')
          $searchPaths += $installDir
        }
      }

      foreach ($searchDir in $searchPaths) {
        if (-not (Test-Path $searchDir)) { continue }
        Write-Verbose "Searching for SDK DLLs in: $searchDir"
        $foundAll = $true
        foreach ($dll in $requiredDlls) {
          $dllFile = Join-Path $searchDir "$dll.dll"
          if (Test-Path $dllFile) {
            try {
              [Reflection.Assembly]::LoadFrom($dllFile) | Out-Null
              Write-Verbose "Loaded '$dll' from: $dllFile"
            }
            catch {
              Write-Verbose "Failed to load '$dllFile': $_"
              $foundAll = $false
              break
            }
          }
          else {
            $foundAll = $false
            break
          }
        }
        if ($foundAll) { return $true }
      }

      # All attempts failed
      Write-Error @"
SCOM SDK DLLs could not be found. The following assemblies are required:
  - Microsoft.EnterpriseManagement.Core.dll
  - Microsoft.EnterpriseManagement.Packaging.dll
 
To resolve, try one of:
  1. Run this function on a machine with SCOM Console or Server installed.
  2. Specify the -SdkPath parameter pointing to the directory containing the DLLs.
  3. Set the SCOM_SDK_PATH environment variable to the SDK Binaries directory.
 
Common locations:
  - C:\Program Files\Microsoft System Center\Operations Manager\Server\SDK Binaries
  - C:\Program Files\Microsoft System Center\Operations Manager\Console\SDK Binaries
"@

      return $false
    }
    #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
    }

    # Load SDK
    $sdkLoaded = Initialize-SdkAssemblies -ExplicitPath $SdkPath
    if (-not $sdkLoaded) { return }

    $allFailed = @()
    $processedCount = 0

    # Collect files for directory mode in Begin so Process handles pipeline
    $filesToProcess = @()
    if ($PSCmdlet.ParameterSetName -eq 'ByDirectory') {
      $filesToProcess = @(Get-ChildItem $InputDir -Include '*.mp','*.mpb' -Recurse -File -ErrorAction SilentlyContinue)
      if ($filesToProcess.Count -eq 0) {
        Write-Warning "No .mp or .mpb files found in '$InputDir'."
        return
      }
      Write-Verbose "Found $($filesToProcess.Count) file(s) to process in '$InputDir'."
    }
  }

  Process {
    # Determine which files to process this iteration
    if ($PSCmdlet.ParameterSetName -eq 'ByPipeline') {
      if ($InputObject.Extension -notin @('.mp', '.mpb')) {
        Write-Warning "Skipping '$($InputObject.Name)' - not a .mp or .mpb file."
        return
      }
      $filesToProcess = @($InputObject)
    }

    foreach ($file in $filesToProcess) {
      $processedCount++
      try {
        if ($file.Extension -eq '.mp') {
          # Process .mp file
          Write-Verbose "Processing MP: $($file.FullName)"
          $sourceDir = Split-Path $file.FullName -Parent
          $mpStore = New-Object Microsoft.EnterpriseManagement.Configuration.IO.ManagementPackFileStore($sourceDir)
          $mp = New-Object Microsoft.EnterpriseManagement.Configuration.ManagementPack($file.FullName)
          $versionStr = 'unknown'
          try { $versionStr = $mp.Version.ToString() } catch {}
          $folderName = "$($file.Name)($versionStr)"
          $targetPath = Join-Path $OutputDir $folderName

          if ((Test-Path $targetPath) -and -not $Force) {
            Write-Verbose "Skipping '$($file.Name)' - output folder exists. Use -Force to overwrite."
            continue
          }
          if (Test-Path $targetPath) { Remove-Item $targetPath -Recurse -Force }
          New-Item -Path $targetPath -ItemType Directory -Force | Out-Null

          $mpWriter = New-Object Microsoft.EnterpriseManagement.Configuration.IO.ManagementPackXmlWriter($targetPath)
          [void]$mpWriter.WriteManagementPack($mp)
          Write-Verbose " -> Exported to: $targetPath"
        }
        elseif ($file.Extension -eq '.mpb') {
          # Process .mpb file
          Write-Verbose "Processing MPB: $($file.FullName)"
          $sourceDir = Split-Path $file.FullName -Parent
          $mpStore = New-Object Microsoft.EnterpriseManagement.Configuration.IO.ManagementPackFileStore($sourceDir)
          $mpbReader = [Microsoft.EnterpriseManagement.Packaging.ManagementPackBundleFactory]::CreateBundleReader()
          $bundle = $mpbReader.Read($file.FullName, $mpStore)
          $versionStr = 'unknown'
          try {
            $firstMp = $bundle.ManagementPacks | Select-Object -First 1
            if ($firstMp -and $firstMp.Version) { $versionStr = $firstMp.Version.ToString() }
          } catch {}
          $folderName = "$($file.Name)($versionStr)"
          $targetPath = Join-Path $OutputDir $folderName

          if ((Test-Path $targetPath) -and -not $Force) {
            Write-Verbose "Skipping '$($file.Name)' - output folder exists. Use -Force to overwrite."
            continue
          }
          if (Test-Path $targetPath) { Remove-Item $targetPath -Recurse -Force }
          New-Item -Path $targetPath -ItemType Directory -Force | Out-Null

          $mpWriter = New-Object Microsoft.EnterpriseManagement.Configuration.IO.ManagementPackXmlWriter($targetPath)
          foreach ($bundleItem in $bundle.ManagementPacks) {
            [void]$mpWriter.WriteManagementPack($bundleItem)
            $streams = $bundle.GetStreams($bundleItem)
            foreach ($stream in $streams.Keys) {
              $mpElement = $bundleItem.FindManagementPackElementByName($stream)
              $fileName = $mpElement.FileName
              if ($null -eq $fileName) {
                $outpath = Join-Path $targetPath ('.' + $stream + '.bin')
              }
              elseif ($fileName.IndexOf('\') -gt 0) {
                $outpath = Join-Path $targetPath (Split-Path $fileName -Leaf)
              }
              else {
                $outpath = Join-Path $targetPath $fileName
              }
              Write-Verbose " -> Resource: $outpath"
              $fs = [System.IO.File]::Create($outpath)
              $streams[$stream].WriteTo($fs)
              $fs.Close()
            }
          }
          Write-Verbose " -> Exported to: $targetPath"
        }
      }
      catch {
        Write-Warning "Failed to process '$($file.Name)': $_"
        $allFailed += $file.Name
      }
    }

    # Reset for next pipeline iteration
    if ($PSCmdlet.ParameterSetName -eq 'ByPipeline') {
      $filesToProcess = @()
    }
  }

  End {
    $successCount = $processedCount - $allFailed.Count
    if ($processedCount -gt 0) {
      Write-Verbose "Processing complete. $successCount of $processedCount file(s) succeeded."
    }
    if ($allFailed.Count -gt 0) {
      Write-Warning "The following $($allFailed.Count) file(s) failed to process:"
      $allFailed | ForEach-Object { Write-Warning " - $_" }
    }
  }

}