SCOMHelper.psm1

#######################################################################
# PRIVATE
#######################################################################
<#
    .Link
    http://jdhitsolutions.com/blog/2013/01/
    .Inputs
    Object
    .Outputs
    Simulated graph
    .Notes
    Version: 1.0?
    (Tyson: I'm not sure where I found this seemingly early version of this function. However, it's obvious to me that this is
    from Jeffery Hicks.)
    Author : Jeffery Hicks (http://jdhitsolutions.com/blog)
#>

Function Out-ConsoleGraph {

  [CmdletBinding()]
  Param(
    [Parameter(Position=0,
    ValueFromPipeline=$true)]
    [Object]
    $Object,
    [Parameter(Mandatory=$true)]
    [String]
    $Property,
    $Columns
  )

  BEGIN
  {
    $Width = $Host.UI.RawUI.BufferSize.Width
    $Data = @()
  }

  PROCESS
  {
    # Add all of the objects from the pipeline into an array
    $Data += $Object
  }

  END
  {
    # Determine scale of graph
    Try
    {
      $Largest = $Data.$Property | Sort-Object | Select-Object -Last 1
    }

    Catch
    {
      Write-Warning "Failed to find property $Property"
      Return
    }

    if ($Largest)
    {
      # Add the width of all requested columns to each object
      $Data = $Data | Select-Object -Property $Columns | ForEach-Object{
        $Lengths = @()
        $Len = 0
        $Item = $_
        $Columns | ForEach-Object{
          if ($Item.$($_))
          {
            $Len += $Item.$($_).ToString().Length
          }
        }
        Add-Member -InputObject $Item -MemberType NoteProperty -Name Length -Value $Len -PassThru
        $Lengths += $Len
      }

      # Determine the available chart space based on width of all requested columns
      $Sample = $Lengths | Sort-Object -Property Length | Select-Object -Last 1
      [Int]$Longest = $Sample.Length + ($Columns.Count * 33)
      $Available = $Width-$Longest-4

      ForEach ($Obj in $Data)
      {
        # Set bar length to 0 if it is not a number greater than 0
        if ($Obj.$Property -eq '-' -OR $Obj.$Property -eq 0 -or -not $Obj.$Property)
        {
          [Int]$Graph = 0
        }
        else
        {
          $Graph = (($Obj.$Property) / $Largest) * $Available
        }

        # Based on bar size, use a different character to visualize the bar
        if ($Graph -ge 2)
        {
          [String]$G = [char]9608
        }
        elseif ($Graph -gt 0 -AND $Graph -le 1)
        {
          [String]$G = [char]9612
          $Graph = 1
        }

        # Create the property that will contain the bar
        $Char = $G * $Graph
        $Obj | Select-Object -Property $Columns | Add-Member -MemberType NoteProperty -Name Graph -Value $Char -PassThru

      } # End ForEach

    } # End if ($Largest)

  } # End of END block

} # End Out-ConsoleGraph
#######################################################################

Function Get-ModuleRunasProfileName {
  Param(
    $Module,
    $Profiles
  )
  $ModuleRunAs = $Profiles | Where-Object { $_.Id.Guid -eq $module.RunAs.Id.GUID }
  Return ($ModuleRunAs.Name+";"+$ModuleRunAs.DisplayName)
}
#######################################################################
# PRIVATE
#######################################################################


<#
    .Synopsis
    Will create a graphical structure (.png file) that represents SCOM class taxonomy; all SCOM class attributes, properties, hosting relationships, and discovery relationships for a SCOM class.
     
    .DESCRIPTION
    This function will use the GraphViz package to produce a graph-like structure (.png file) that will represent one or more SCOM
    classes and it's full hierarchy which includes parents, attributes, properties, hosting relationships (both hosted and hosting), and any discoveries which are capable of discovering the related classes.
    Any number of SCOM class objects or names may be piped to the function.
 
    This function relies on the following modules. User will be prompted for permissio to install these:
    OpsMgrExtended: available from the PowerShell Gallery here: https://www.powershellgallery.com/packages/OpsMgrExtended
    GraphViz: from the Chocolatey repo. (https://www.graphviz.org/)
    PSGraph: https://github.com/KevinMarquette/PSGraph
 
    .EXAMPLE
    PS C:\> New-SCOMClassGraph -ClassName 'Microsoft.SQLServer.2012.DBEngine'
    .EXAMPLE
    PS C:\> New-SCOMClassGraph -ClassName 'Microsoft.SQLServer.2014.AlwaysOn.DatabaseReplica' -Caching:$False
 
    The above example will generate a new graph even if one already exists in the default storage directory.
    .EXAMPLE
    PS C:\> New-SCOMClassGraph -ClassName 'Microsoft.Windows.Server.6.2.LogicalDisk' -ShowDiscoveries:$false
 
    The above example will create a graph but will not include Discovery relationships.
    NOTE: If you are not seeing discovery data in your graphs it may be because the graphs are cached without discovery data.
    Try using the caching switch to force new graph files to be created. Discovery data will be included by default.
     
    Example: -Caching:$false
    .EXAMPLE
    PS C:\> Get-SCOMClass -Name *sql* | New-SCOMClassGraph -ShowGraph:$false
 
    The above example will create graph files for ALL SQL classes but will not open the graph files in the default application.
    Typically there are a tremendous number of SQL classes in the SQL management packs.
     
    .EXAMPLE
    PS C:\> New-SCOMClassGraph -ID 'ea99500d-8d52-fc52-b5a5-10dcd1e9d2bd'
     
    .EXAMPLE
    PS C:\> (Get-SCOMClassInstance -DisplayName "http://ms01.contoso.com:80/" ).getclasses().Name | New-SCOMClassGraph
 
    The above example will show the class graph for a specific instance of a class type.
     
    .EXAMPLE
    PS C:\> (Get-SCOMClassInstance -DisplayName "SQLEXPRESS" ).getclasses().Name | New-SCOMClassGraph
 
    The above example will show the class graph for a specific instance of a class type.
 
    .EXAMPLE
    PS C:\> New-SCOMClassGraph -ShowIndex
 
    The example above will display an html index page for all of the graph files. The html file will open with the default application.
 
    .EXAMPLE
    PS C:\> Get-SCOMClass | Select Name,Displayname, @{N='ManagementPackName';E={$_.Identifier.Domain[0]} }| Out-GridView -PassThru | New-SCOMClassGraph
 
    This example above is a much fancier way to select one or more class names with the help of GridView. This command will get all SCOM classes in the management group and present Name, DisplayName, and ManagementPackName in GridView for easy browsing, filtering, and selection by the user. Select one or more classes from the Grid View, click OK. The selected class name(s) gets piped into the function: New-SCOMClassGraph
 
    .EXAMPLE
    #Example Script
 
    $int=0
    $arr =@()
    ForEach ($class in (Get-SCOMClass)) {
    $obj = New-Object pscustomobject
    $obj | add-member -name 'Index' -Value $int -MemberType NoteProperty
    $obj | add-member -name 'Name' -Value ($Class.Name) -MemberType NoteProperty
    $obj | add-member -name 'DisplayName' -Value ($class.DisplayName) -MemberType NoteProperty
    $arr+=$obj
    $int++
    }
    $arr | Out-GridView -PassThru | New-SCOMClassGraph -Caching:$false -Combine
 
    This example will present a gridview list of classes for you to select. Multi-select supported to combine numerous classes on a single graph. Be careful, don't overdo it.
 
    .Parameter ClassName
    This is the name of a class. (not the DisplayName)
     
    .Parameter Class
    An Operations Manager class object.
 
    .Parameter ID
    ID of a class.
 
    .Parameter Caching
    This will enable/disable the use of preexisting graphs. If $false, a new graph will be created. If $true, the script will look for an existing graph for the class. If no graph exists, a new one will be created.
     
    .Parameter ShowGraph
    This allows the user to generate the graphs in the designated directory without opening them (with the default application associated to the .png file type)
     
    .Parameter ShowDiscoveries
    This will allow the user to omit the discovery relationships.
     
    .Parameter ShowIndex
    This will display an html index page for all of the graph files. The html file will open with the default application.
     
    .NOTES
    Author: Tyson Paul
    Blog: https://blogs.msdn.microsoft.com/tysonpaul/2018/07/18/scomhelper-powershell-module-a-scom-admins-best-friend/
 
    History
    2019.06.25 - Fixed -Combine flaw
    2019.06.24 - Added input field and search feature to index html
    2019.06.06 - Improved Combine functionality.
                 Added -ShowIndex switch
    2019.04.29 - Added colored connected arrows to better identify relationships.
    2018.06.14 - Initial release.
 
    .LINK
    Get-SCOMMPFileInfo
    Get-SCOMClassInfo
#>

Function New-SCOMClassGraph {
  [CmdletBinding(DefaultParameterSetName='Parameter Set 1',
      SupportsShouldProcess=$true,
      PositionalBinding=$false,
      HelpUri = 'https://blogs.msdn.microsoft.com/tysonpaul/',
  ConfirmImpact='Medium')]
  Param (

    [Parameter(Mandatory=$true,
        ValueFromPipeline=$true,
        ValueFromRemainingArguments=$false,
        Position=0,
    ParameterSetName='Parameter Set 1')]
    [Microsoft.EnterpriseManagement.Configuration.ManagementPackClass[]]$Class,

    [Parameter(Mandatory=$true,
        ValueFromPipeline=$true,
        ValueFromPipelineByPropertyName=$true,
        ValueFromRemainingArguments=$false,
        Position=0,
    ParameterSetName='Parameter Set 2')]
    [alias('Name', 'DisplayName')]
    [string[]]$ClassName,

    [Parameter(Mandatory=$true,
        ValueFromPipeline=$false,
        ValueFromRemainingArguments=$false,
        Position=0,
    ParameterSetName='Parameter Set 3')]
    [string[]]$ID,

    [Parameter(Mandatory=$false,
    ParameterSetName='Parameter Set 1')]
    [Parameter(Mandatory=$false,
    ParameterSetName='Parameter Set 2')]
    [Parameter(Mandatory=$false,
    ParameterSetName='Parameter Set 3')]
    [string]$outDir,

    # This will enable/disable the use of a previous graph file. Disable: will create a new/fresh graph file.
    [Parameter(Mandatory=$false,
    ParameterSetName='Parameter Set 1')]
    [Parameter(Mandatory=$false,
    ParameterSetName='Parameter Set 2')]
    [Parameter(Mandatory=$false,
    ParameterSetName='Parameter Set 3')]
    [switch]$Caching=$false,

    # When combining multiple classes on the same graph the host node color might not appear correctly. However, any arrows will indicate the relationship type based on color. This will depend entirely on which classes get enumerated first.
    [Parameter(Mandatory=$false,
    ParameterSetName='Parameter Set 1')]
    [Parameter(Mandatory=$false,
    ParameterSetName='Parameter Set 2')]
    [Parameter(Mandatory=$false,
    ParameterSetName='Parameter Set 3')]
    [switch]$Combine,

    # This will open the graph file with the default application (.png)
    [Parameter(Mandatory=$false,
    ParameterSetName='Parameter Set 1')]
    [Parameter(Mandatory=$false,
    ParameterSetName='Parameter Set 2')]
    [Parameter(Mandatory=$false,
    ParameterSetName='Parameter Set 3')]
    [switch]$ShowGraph=$true,

    # This will include discovery nodes in the graph(s)
    [Parameter(Mandatory=$false,
    ParameterSetName='Parameter Set 1')]
    [Parameter(Mandatory=$false,
    ParameterSetName='Parameter Set 2')]
    [Parameter(Mandatory=$false,
    ParameterSetName='Parameter Set 3')]
    [switch]$ShowDiscoveries=$true,

    # This will create/update, then open the index file with the default application (.html)
    [Parameter(Mandatory=$false,
        Position=0,
    ParameterSetName='Parameter Set 4')]
    [switch]$ShowIndex
  )

  Begin{
    #######################################################################

    
    Import-Module OperationsManager,PSGraph,PowerShellGet -ErrorAction SilentlyContinue

    If (-NOT ($PSVersionTable.PSVersion.Major -ge 5)){
      Write-Error "PowerShell version: $($PSVersionTable.PSVersion.Major) detected! "
      Write-Error "Upgrade to PowerShell version 5 or greater to use this function."
      Break
    }

    'OperationsManager' | ForEach-Object {
      If (-Not [bool](Get-Module -Name $_ -ErrorAction SilentlyContinue )) {
        Write-Error "Required module '$_' does not exist. Please install the module or run this function from a machine where the Operations Manager Console exists. Typically the required module will exists wherever the Console has been installed. Exiting."
        Break
      }
    }

    # Make sure GraphViz package is installed.
    If (-NOT ([bool](Get-Package -Name 'GraphViz' -ProviderName 'Chocolatey' -ErrorAction SilentlyContinue))){
      Write-Error "Required package 'GraphViz' does not exist. Please install package."
      $choice = Read-Host "Install module: GraphViz ? (Y/N)"
      While ($choice -notmatch '[y]|[n]'){
        $choice = Read-Host "Y/N?"
      }
      Switch ($choice){
        'y' {
          # Install 'GraphViz' from the Powershell Gallery
          Write-Host "Find-Package -Name 'GraphViz' -Source 'Chocolatey' | Install-Package -Verbose" -F Gray
          Find-Package -Name 'GraphViz' -Source "Chocolatey" | Install-Package -Verbose
          If ($?){ Write-Host "Package installed!" }
          Else { Write-Error "Problem installing package.`nExiting.`n"; Exit}
        }
        'n' {
          Write-Host "Package will not be installed.`nExiting.`n"
          Break
        }
      }
    }

    # Make sure PSGraph is available/loaded
    # https://github.com/KevinMarquette/PSGraph
    If (-Not [bool](Get-Module -Name 'PSGraph' -ErrorAction SilentlyContinue  )) {
      Write-Error "Required module 'PSGraph' does not exist. Please install module."
      $choice = Read-Host "Install module: PSGraph (Y/N) ?"
      While ($choice -notmatch '[y]|[n]'){
        $choice = Read-Host "Y/N?"
      }
      Switch ($choice){
        'y' {
          # Install PSGraph from the Powershell Gallery
          Write-Host "Find-Module PSGraph | Install-Module -Verbose"
          Find-Module PSGraph | Install-Module -Verbose
          If ($?){
            Write-Host "Module installed!"
            Import-Module PSGraph
          }
          Else {
            Write-Error "Problem installing module.
              You may manually download from this location: 'https://github.com/KevinMarquette/PSGraph'. Additional information here: 'https://kevinmarquette.github.io/2017-01-30-Powershell-PSGraph/'.
            For information on how to install a PowerShell module, see this article: 'https://msdn.microsoft.com/en-us/library/dd878350(v=vs.85).aspx'.`nExiting.`n"
;
            Break
          }
        }
        'n' {
          Write-Host "Module will not be installed.`nExiting.`n"
          Break
        }
      }
    }

    ###############################################################
    # This is a customized version of the original function located in the PSGraph module here: https://github.com/KevinMarquette/PSGraph
    Function Record {
      <#
          .SYNOPSIS
          Creates a record object for GraphViz.
          This is a customized version of the "Record" function that exists in the PSGraph module from Kevin Marquette.
          I added some additional parameters to control font styles.
      #>

      [OutputType('System.String')]
      [cmdletbinding(DefaultParameterSetName = 'Script')]
      param(
        [Parameter(
            Mandatory=$true,
            Position = 0
        )]
        [alias('ID', 'Node')]
        [string]
        $Name,

        [Parameter(
            Position = 1,
            ValueFromPipeline=$true,
            ParameterSetName = 'Strings'
        )]
        [alias('Rows')]
        [Object[]]
        $Row,

        [Parameter(
            Position = 1,
            ParameterSetName = 'Script'
        )]
        [ScriptBlock]
        $ScriptBlock,

        [Parameter(
            Position = 2
        )]
        [ScriptBlock]
        $RowScript,

        [string]
        $Label,

        # Added for color customization of header
        [string]$FONTCOLOR = 'white',
        [string]$BGCOLOR = 'black',

        # Added for color customization of table
        [string]$FILLCOLOR = 'white',
        [string]$STYLE = 'filled',
        [string]$TABLECOLOR = 'black'

      )
      begin
      {
        $tableData = [System.Collections.ArrayList]::new()
        if ( [string]::IsNullOrEmpty($Label) )
        {
          $Label = $Name
        }
      }
      process
      {
        if ( $null -ne $ScriptBlock )
        {
          $Row = $ScriptBlock.Invoke()
        }

        if ( $null -ne $RowScript )
        {
          $Row = foreach ( $node in $Row )
          {
            @($node).ForEach($RowScript)
          }
        }

        $results = foreach ( $node in $Row )
        {
          Row -Label $node
        }

        foreach ( $node in $results )
        {
          [void]$tableData.Add($node)
        }
      }
      end
      {
        #$html = '<TABLE CELLBORDER="1" BORDER="0" CELLSPACING="0"><TR><TD bgcolor="black" align="center"><font color="white"><B>{0}</B></font></TD></TR>{1}</TABLE>' -f $Label, ($tableData -join '')
        $html = '<TABLE CELLBORDER="1" COLOR="'+$TABLECOLOR+'" BORDER="0" CELLSPACING="0"><TR><TD bgcolor="'+$BGCOLOR+'" align="center"><font color="'+$FONTCOLOR+'"><B>{0}</B></font></TD></TR>{1}</TABLE>' -f $Label, ($tableData -join '')
        #Node $Name @{label = $html; shape = 'none'; fontname = "Courier New"; style = "filled"; penwidth = 1; fillcolor = "white"}
        Node $Name @{label = $html; shape = 'none'; fontname = "Courier New"; style = $STYLE; penwidth = 1; fillcolor = $FILLCOLOR}
      }
    }#End Function
    ###############################################################

    Function Dig-Class {
      [CmdletBinding(DefaultParameterSetName='Parameter Set 1',
          SupportsShouldProcess=$true,
          PositionalBinding=$false,
      ConfirmImpact='Medium')]
      [Alias()]
      [OutputType([System.Object[]])]

      Param(
        [Parameter(Mandatory=$true,
            ValueFromPipeline=$true,
            ValueFromPipelineByPropertyName=$false,
            ValueFromRemainingArguments=$false,
            Position=0,
        ParameterSetName='Parameter Set 1')]
        [ValidateNotNull()]
        [ValidateNotNullOrEmpty()]
        [Microsoft.EnterpriseManagement.Configuration.ManagementPackClass]$Class,

        # Determines if this Class object is hosting another class. Will affect the shape/color
        [Parameter(Mandatory=$false,
            ValueFromPipeline=$false,
            Position=1,
        ParameterSetName='Parameter Set 1')]
        [switch]$IsHosting=$false,

        [System.Collections.Hashtable]$hashCollection=@{}
      )
      Begin {}
      Process{
        Write-Host $Class.Name -ForegroundColor Yellow
            
        $hashThisClass = @{}
        # If a base class exists, dig it
        If ([bool]$Class.Base.ID.Guid){
          $BaseClass = (Get-SCClass -ID $Class.Base.ID.Guid )
          If (-NOT ($hashCollection[$BaseClass.Name])) {
            $hashCollection = (Dig-Class $BaseClass -hashCollection $hashCollection) 
          }
          Try{
              $hashCollection[$BaseClass.Name]["Edges"] += @{($Class.Name)=('child')} 
          }Catch{
              #Empty Catch is fine here
          }
        }

        # Make sure that if a class is hosting that its 'Hosting' flag gets set. This is important when multiple class targets are combined on a single graph.
        If (($IsHosting) -AND ($hashCollection[$Class.Name]) ) {
          $hashCollection[$Class.Name].Hosting = $True
        }
        
        # If a hosting class exists, dig it.
        If ($Class.Hosted){
          $hashGraph2= @{}
          $hostClass = $Class.FindHostClass()
          If (-NOT ($hashCollection[$hostClass.Name])) {
            $hashHosting = (Dig-Class -Class $hostClass -IsHosting -hashCollection $hashCollection)
            $hashGraph2 = (Merge-HashTables -hmaster $hashCollection -htnew $hashHosting)
            $hashCollection = $hashGraph2
          }
          Else {
            $hashCollection[$hostClass.Name].Hosting = $True
          }

          Try{
              $hashCollection[$hostClass.Name]["Edges"] += @{($Class.Name)=('hosted')}
          }Catch{
              #Empty Catch is fine here
          }
        }
        #endregion

        [System.Object[]]$PropNames =@()
        If ([bool]$Class.DisplayName) {
          $PropNames += Stylize-Row -Color $c_classAttribute -Bold $s_ClasAttributeBold -thisString "DisplayName: $($Class.DisplayName.ToString())"
        }

        $PropNames += Stylize-Row -Color $c_classAttribute -Bold $true -thisString "MP: $($Class.ManagementPackName)"

        If ($Class.Hosted) {
          $PropNames += Stylize-Row -Color $c_classAttribute -Bold $true -thisString "Hosted: $($Class.Hosted.ToString())"
        }
        Else {
          $PropNames += Stylize-Row -Color $c_classAttribute -Bold $s_ClasAttributeBold -thisString "Hosted: $($Class.Hosted.ToString())"
        }

        If ([bool]$Class.Abstract) {
          $PropNames += Stylize-Row -Color $c_classAttribute -Bold $TRUE -thisString "Abstract: $($Class.Abstract.ToString())"
        }
        Else{
          $PropNames += Stylize-Row -Color $c_classAttribute -Bold $s_ClasAttributeBold -thisString "Abstract: $($Class.Abstract.ToString())"
        }

        If ([bool]$Class.Extension) {
          $PropNames += Stylize-Row -Color $c_ExtensionFont -Bold $s_ClasAttributeBold -thisString "Extension: $($Class.Extension.ToString())"
        }
        Else {
          $PropNames += Stylize-Row -Color $c_classAttribute -Bold $s_ClasAttributeBold -thisString "Extension: $($Class.Extension.ToString())"
        }
        # Get all properties of the class
        $PropNames += $Class.GetProperties().Name | Sort-Object

        # Identify the Key property (if any)
        $KeyName = $Class.GetProperties() | Where-Object {$_.Key -eq $true} | Select-Object -Property Name -ExpandProperty Name

        # If a Key property exists, format it as BOLD
        If ([Bool]$KeyName ) {
          $PropNames = $PropNames | Where-Object { (-not ([string]::IsNullOrEmpty($_))) } |ForEach-Object {$_ -Replace "^$KeyName$","<Font Color=`"Red`"><B>$($_)</B></Font>"}
        }

        $hashStyling = Get-DefaultStyling
        $hashThisClass = [Ordered]@{
          #"Edges" = $Edges
          "PropNames" = $PropNames
          "Abstract" = $Class.Abstract
          "Hosted" = $Class.Hosted
          "Hosting" = $IsHosting
          "Styling" = $hashStyling
        }

        # Keep any previous dig results, specifically Edges already identified.
        If ([bool]($hashCollection[$Class.Name].Edges)){
          $hashThisClass.Edges = $hashCollection[$Class.Name].Edges
        }

        $hashCollection[$Class.Name] = $hashThisClass
        Return $hashCollection
      }#end Process
      End {}
    }
    ###############################################################

    Function Merge-HashTables {
      param(
        $hmaster,

        $htnew
      )
      $hmaster.keys | ForEach-Object {
        $key = $_
        If (-NOT $htnew.containskey($key)) {
          $htnew.Add($key,$hmaster.$key)
        }
        Else {
          ForEach ($E in @($hmaster[$_].Edges.Keys)) {
            Try{
              If (-NOT [System.Object]$htnew[$_].Edges.ContainsKey($E) ){
                [System.Object]$htnew[$_].Edges.Add($E,$hmaster[$_].Edges.$E) 
              } 
            }Catch {
              # Empty catch is fine here
            }
          }
        }
      }
      #$htnew = $htold + $htnew
      return $htnew
    }
    ###############################################################

    Function Stylize-Record {
      # Modify colors for any hosted/hosting records
      Param(
        $hashtable
      )
      $hashtable.Keys | ForEach-Object{
        #<#
        If ($hashtable[$_].Abstract -eq $true){
          # Table Header
          $hashtable[$_].Styling.FONTCOLOR = $c_AbstHdrFont
          $hashtable[$_].Styling.BGCOLOR = $c_AbstHdrBG
          # Table/Rows
          $hashtable[$_].Styling.FILLCOLOR = $c_AbstFill
          $hashtable[$_].Styling.Style = $c_AbstStyle
          $hashtable[$_].Styling.TABLECOLOR = $c_AbstTable
        }
        #>
        If ($hashtable[$_].Hosted -eq $true) {
          # Table Header
          $hashtable[$_].Styling.FONTCOLOR = $c_hostedHdrFont
          $hashtable[$_].Styling.BGCOLOR = $c_hostedHdrBG
          # Table/Rows
          $hashtable[$_].Styling.FILLCOLOR = $c_hostedFill
          $hashtable[$_].Styling.Style = $c_hostedStyling
          $hashtable[$_].Styling.TABLECOLOR = $c_hostedTable
        }

        If ($hashtable[$_].Hosting -eq $true){
          # Table Header
          $hashtable[$_].Styling.FONTCOLOR = $c_hostingHdrFont
          $hashtable[$_].Styling.BGCOLOR = $c_hostingHdrBG
          # Table/Rows
          $hashtable[$_].Styling.FILLCOLOR = $c_hostingFill
          $hashtable[$_].Styling.Style = $c_hostingStyle #"filled"
          #$hashtable[$_].Styling.TABLECOLOR = $c_hostingTable
        }

        # Make sure the Abstract classes retain their header bg color
        If ($hashtable[$_].Abstract -eq $true){
          # Table Header
          $hashtable[$_].Styling.BGCOLOR = $c_AbstHdrBG
        }

        If ($hashtable[$_].Discovery -eq $true){
          # Table Header
          $hashtable[$_].Styling.FONTCOLOR = $c_DiscHdrFont
          $hashtable[$_].Styling.BGCOLOR = $c_DiscHdrBG
          # Table/Rows
          $hashtable[$_].Styling.FILLCOLOR = $c_DiscFill
          $hashtable[$_].Styling.Style = $c_DiscStyle
          $hashtable[$_].Styling.TABLECOLOR = $c_DiscTable
        }
      }
      Return $hashtable
    }
    ###############################################################

    Function Stylize-Row {
      [CmdletBinding(DefaultParameterSetName='Parameter Set 1',
          SupportsShouldProcess=$true,
          PositionalBinding=$false,
      ConfirmImpact='Medium')]
      [Alias()]
      [OutputType([System.Object[]])]
      Param(
        [Parameter(Mandatory=$true,
            ValueFromPipeline=$true,
            ValueFromPipelineByPropertyName=$false,
            ValueFromRemainingArguments=$false,
            Position=0,
        ParameterSetName='Parameter Set 1')]
        [ValidateNotNull()]
        [ValidateNotNullOrEmpty()]
        [System.String[]]$thisString,

        [string]$Color = 'black',
        [bool]$Bold = $false

      )
      Begin{}
      Process{
        ForEach ($string in $thisString){

          $newString = ($String -replace '(<Font)|(Color="[a-zA-Z]*">)|(<B>)|(<\/B>)|(<\/Font>)','' )
          $newString = $newString | Where-Object { (-not ([string]::IsNullOrEmpty($_))) } | ForEach-Object -Process {$_ -Replace "^$_$","<Font Color=`"$($Color)`"><B>$($_)</B></Font>"}
          If (-NOT $Bold){
            $newString = ($newString -replace '(<B>)|(</B>)','' )
          }
          Return $newString
        }
      }
      End{}
    }
    ###############################################################

    Function Get-DefaultStyling {
      # Default Record style
      $hashStyling = [Ordered]@{
        # Table Header
        FONTCOLOR = $c_defaultHdrFont
        BGCOLOR = $c_defaultHdrBG

        # Table/Rows
        FILLCOLOR = $c_defaultFill
        Style = $c_defaultStyle
        TABLECOLOR = $c_defaultTable
      }
      Return $hashStyling
    }
    ###############################################################

    Function Get-Discoveries {
      Param(
        [hashtable]$hashMaster,
        [hashtable]$hashDiscoveries = @{}
      )

      #$discHash = @{}
      $discHash = $hashDiscoveries
      # Get only discoveries which potentially create classes (not relationships)
      $discs = Get-SCDiscovery | Where-Object { [bool]$_.DiscoveryClassCollection}

      ForEach ($disc in $discs) {
        If ($disc.DiscoveryClassCollection.Count -gt 1){
          $c=1
        }
        Else {
          $c=$null
        }

        ForEach ($discCollection in $disc.DiscoveryClassCollection) {
          $thisHash = @{}
          If ($hashMaster.containsKey([string]$discCollection.typeid.Identifier.path) -and (-NOT $thisHash.ContainsKey([string]$discCollection.typeid.Identifier.path)) ) {
            $PropNames = @()
            $PropNames += Stylize-Row -thisString "DisplayName: $($Disc.DisplayName)" -Color $c_DiscDisplayName -Bold $true
            $PropNames += Stylize-Row -thisString "Target: $([string]$disc.target.Identifier.Path)" -Color $c_DiscTarget -Bold $true
            $PropNames += Stylize-Row -thisString "MP: $([string]$Disc.Identifier.Domain[0])" -Color $c_DiscTarget -Bold $true
            

            ForEach ($PropID in $discCollection.propertycollection.PropertyID ) {
              $PropNames += Stylize-Row -thisString $PropID -Color $c_DiscRow
            }
            $thisHash.Add('PropNames',$PropNames)
            $thisHash.'Edges' += @([string]$discCollection.typeid.Identifier.path)
            $thisHash.'Label' = $disc.Name
            $thisHash.'Discovery' = $true
            $thisHash.'Styling' = Get-DefaultStyling
            $c++
            Try{
                $discHash.Add(([string]$disc.Name+$c),$thisHash) 
            }Catch{ 
                #empty Catch is fine here
            }
          }
        }
      }
      Return $discHash
    }
    ###############################################################

    Function New-Graph {

      # If all classes are meant to be combined into a single graph, then the filename will be calculated with a hash function.
      If ($Combine) {
        $hashstring = Get-StringHash -String ($hashGraph.GetEnumerator() | Out-String)
        $outPath = (Join-Path $OutDir ("MD5Hash_$($hashstring)" +".png" ))
      }

      Graph {
        #region Classes
        #$hashGraph.Keys | ForEach-Object -Process {
        ForEach ($ClassKey in @($hashGraph.Keys)) {
          # These will determine if Hosted/Hosting nodes appear in the Legend at the top of the .png
          If ([bool]$hashGraph[$ClassKey].Hosted) {$HostedExists = $true}
          If ([bool]$hashGraph[$ClassKey].Hosting) {$HostingExists = $true}

          $params = @{
            Name = $ClassKey
            Row = $hashGraph[$ClassKey].PropNames
          }
          $params += $hashGraph[$ClassKey].Styling
          Record @params
          #<#
          ForEach ($E in @($hashGraph[$ClassKey].Edges.Keys | Where-Object {$_.Length -gt 0}) ) {
            If ([bool]$hashGraph.$E.Hosted) {
              $EdgeStyle = "bold"
            }
            Else {
              $EdgeStyle = "bold"
            }          
            [string]$edgeColor = ($hashEdgeColor[$hashGraph[$ClassKey].Edges.$E])
            Edge -From $ClassKey -To $E @{color=$edgeColor;style=$EdgeStyle}
          }
          #>
        }
        #endregion Classes

        If ($ShowDiscoveries) {
          #region Discoveries
          $hashDiscoveries.Keys | ForEach-Object {
            $Params = @{
              Name = $_
              Label = $hashDiscoveries[$_].Label
              Row = $hashDiscoveries[$_].PropNames
            }
            $Params += $hashDiscoveries[$_].Styling
            Record @Params
            [string]$edgeColor = ($hashEdgeColor['Discovery'])
            Edge -From $_ -To ($hashDiscoveries[$_].Edges | Select-Object -Unique) @{color=$edgeColor}
          }
          #endregion Discoveries
        }

        #region Legend
        SubGraph -Attributes @{label='Legend'} -ScriptBlock {

          If ($HostedExists) {
            # Output Legend nodes
            Record -Name "Hosted Class" `
            -Row `
            @((Stylize-Row -thisString 'Class Attribute' -Color $c_classAttribute -Bold $s_ClasAttributeBold),`
              (Stylize-Row -thisString 'Key Property' -Color $c_Key -Bold $s_KeyBold),`
            'Property') `
            -BGCOLOR $c_hostedHdrBG `
            -FONTCOLOR $c_hostedHdrFont `
            -FILLCOLOR $c_hostedFill `
            -STYLE $c_hostedStyling `
            -TABLECOLOR $c_hostedTable
          }

          If ($HostingExists) {
            Record -Name "Hosting Class" `
            -Row `
            @((Stylize-Row -thisString 'Class Attribute' -Color $c_classAttribute -Bold $s_ClasAttributeBold),`
              (Stylize-Row -thisString 'Key Property' -Color $c_Key -Bold $s_KeyBold),`
            'Property') `
            -BGCOLOR $c_hostingHdrBG `
            -FONTCOLOR $c_hostingHdrFont `
            -FILLCOLOR $c_hostingFill `
            -STYLE $c_hostingStyle `
            -TABLECOLOR $c_hostingTable
          }

          Record -Name "Abstract Class" `
          -Row `
          @((Stylize-Row -thisString 'Class Attribute' -Color $c_classAttribute -Bold $s_ClasAttributeBold),`
            (Stylize-Row -thisString 'Key Property' -Color $c_Key -Bold $s_KeyBold),`
          'Property') `
          -BGCOLOR $c_AbstHdrBG `
          -FONTCOLOR $c_AbstHdrFont `
          -FILLCOLOR $c_AbstFill `
          -STYLE $c_AbstStyle `
          -TABLECOLOR $c_AbstTable

          Record -Name "Class" `
          -Row `
          @((Stylize-Row -thisString 'Class Attribute' -Color $c_classAttribute -Bold $s_ClasAttributeBold),`
            (Stylize-Row -thisString 'Key Property' -Color $c_Key -Bold $s_KeyBold),`
          'Property') `
          -BGCOLOR $c_defaultHdrBG `
          -FONTCOLOR $c_defaultHdrFont `
          -FILLCOLOR $c_defaultFill `
          -STYLE $c_defaultStyle `
          -TABLECOLOR $c_defaultTable

          If ($ShowDiscoveries -and ([bool]$hashDiscoveries.Count)) {
            Record -Name "Discovery" `
            -Row `
            (Stylize-Row -thisString 'Discovered Property' -Color $c_DiscRow )`
            -BGCOLOR $c_DiscHdrBG `
            -FONTCOLOR $c_DiscHdrFont `
            -FILLCOLOR $c_DiscFill `
            -STYLE $c_DiscStyle `
            -TABLECOLOR $c_DiscTable
          }
        } #end SubGraph
        #endregion Legend

      } | Export-PSGraph -DestinationPath $outPath -ShowGraph:$ShowGraph
      #--------------------------------

    }
    #######################################################################
    <#
        .Synopsis
        Will create an html index file for all of the graphs present in the default directory.
        .DESCRIPTION
        This function will enumerate all of the .png files in the default ClassGraph directory and build an html file with links to all of the graphs.
 
        .EXAMPLE
        PS C:\> New-SCOMClassGraphIndex -indexFileName 'C:\SCOMgraphs\MySCOMClassGraphs.html'
        The above example will generate the index file at the location specified.
 
        .EXAMPLE
        PS C:\> New-SCOMClassGraphIndex
        The above example will simply generate a new index at the default location.
 
        .EXAMPLE
        PS C:\> New-SCOMClassGraphIndex -ShowIndex
        The above example will generate a new index at the default location and then open it with the default program.
 
 
        .Parameter Dir
        The output directory where the .png files should be located. This is also where the html index file will be stored.
 
        .Parameter filterExt
        The type of files to locate in the output directory. The index will be built from files of this type.
 
        .Parameter indexFileName
        The name of the index file to create.
 
        .Parameter ShowIndex
        This will open the index file with the default program.
 
        .NOTES
        Author: Tyson Paul
        Blog: https://blogs.msdn.microsoft.com/tysonpaul/2018/07/18/scomhelper-powershell-module-a-scom-admins-best-friend/
 
        History
        2019.06.03 - Published to SCOMHelper module
 
        .LINK
        New-SCOMClassGraph
 
    #>

    Function New-SCOMClassGraphIndex {
      Param (
        [string]$outDir,
        [string]$filterExt='png',
        [string]$indexFileName='ClassGraphIndex.html',
        [switch]$ShowIndex
      )

      If (-Not $outDir ) {
        Write-Verbose "No directory provided for index file. "
        Return
      }
      If (-NOT(Test-Path -Path $outDir -PathType Container)) {
        Write-Verbose "No directory found for index file. "
        Return
      }

      $SearchIconFileName = 'searchicon.png'
      [System.Byte[]]$SearchIcon = @(137,80,78,71,13,10,26,10,0,0,0,13,73,72,68,82,0,0,0,21,0,0,0,21,8,6,0,0,0,169,23,165,150,0,0,0,1,115,82,71,66,0,174,206,28,233,0,0,0,4,103,65,77,65,0,0,177,143,11,252,97,5,0,0,0,9,112,72,89,115,0,0,18,116,0,0,18,116,1,222,102,31,120,0,0,0,2,98,75,71,68,0,255,135,143,204,191,0,0,0,9,118,112,65,103,0,0,1,42,0,0,1,41,0,80,22,101,49,0,0,0,37,116,69,88,116,100,97,116,101,58,99,114,101,97,116,101,0,50,48,49,51,45,48,52,45,49,48,84,48,54,58,53,57,58,48,55,45,48,55,58,48,48,142,65,137,81,0,0,0,37,116,69,88,116,100,97,116,101,58,109,111,100,105,102,121,0,50,48,49,51,45,48,52,45,49,48,84,48,54,58,53,57,58,48,55,45,48,55,58,48,48,255,28,49,237,0,0,0,25,116,69,88,116,83,111,102,116,119,97,114,101,0,119,119,119,46,105,110,107,115,99,97,112,101,46,111,114,103,155,238,60,26,0,0,0,17,116,69,88,116,84,105,116,108,101,0,115,101,97,114,99,104,45,105,99,111,110,194,131,236,125,0,0,2,42,73,68,65,84,56,79,165,148,73,171,234,64,16,133,43,237,172,32,142,224,74,17,87,46,149,44,4,17,197,127,237,74,112,4,5,193,165,162,162,91,39,156,16,231,251,238,41,210,33,209,168,240,238,7,33,73,119,245,169,234,170,234,86,126,126,33,3,247,251,157,166,211,41,173,86,43,58,30,143,60,230,118,187,41,24,12,82,34,145,224,239,111,152,68,7,131,1,141,199,99,114,58,157,36,132,32,69,81,120,28,38,143,199,131,174,215,43,197,98,49,202,100,50,60,254,14,93,180,211,233,208,102,179,33,135,195,193,19,136,24,66,0,226,118,187,157,191,49,14,155,98,177,200,255,86,176,40,34,156,205,102,28,161,20,75,165,82,20,14,135,57,98,56,27,141,70,60,7,241,219,237,70,129,64,128,84,85,213,100,204,40,191,6,63,149,74,133,60,30,15,139,33,103,249,124,94,155,54,211,235,245,104,189,94,179,240,233,116,162,92,46,199,226,207,8,20,5,17,2,136,190,19,4,217,108,150,109,97,135,20,32,255,86,8,84,25,91,196,214,176,229,111,164,211,105,182,197,154,229,114,169,141,154,17,104,27,20,2,222,35,145,136,54,252,158,80,40,196,182,88,35,187,226,25,161,21,159,145,45,244,9,68,104,196,82,84,54,51,4,183,219,45,127,127,2,54,50,74,32,91,205,136,192,73,129,55,155,205,198,109,243,141,225,112,200,66,16,181,170,60,16,241,120,156,46,151,11,111,11,253,215,239,247,181,169,87,224,20,61,43,11,155,76,38,181,25,51,194,235,245,82,52,26,101,65,68,48,159,207,169,217,108,114,63,74,118,187,29,117,187,93,154,76,38,220,82,136,18,15,214,89,161,31,211,106,181,170,159,24,164,3,78,100,17,16,25,210,131,7,230,50,167,216,97,185,92,214,143,182,196,116,161,180,219,109,46,4,162,121,238,4,152,193,41,222,16,193,60,156,226,146,41,149,74,228,114,185,52,203,39,81,176,88,44,248,164,32,119,82,24,139,253,126,63,31,14,116,11,156,227,45,35,62,159,207,124,193,224,168,131,23,81,35,48,6,198,40,0,118,211,106,181,94,132,11,133,2,249,124,190,207,162,159,216,239,247,212,104,52,76,194,184,100,16,241,127,139,130,195,225,64,245,122,93,23,70,154,80,232,63,137,2,220,29,181,90,77,191,233,208,41,127,22,5,200,39,250,24,157,160,170,42,253,3,11,167,101,180,126,138,179,206,0,0,0,0,73,69,78,68,174,66,96,130)

      # This is the cute, little 'search' icon that appears on the master class list search field
      $SearchIconPath = Join-Path $OutDir "CSS\$($SearchIconFileName)"
      If (-NOT(Test-Path -Path ($SearchIconPath) -PathType Leaf )) {
          New-Item -Path (Join-Path $OutDir "CSS") -ItemType Container -Force -ErrorAction SilentlyContinue | Out-Null
          $SearchIcon | Set-Content -Path $SearchIconPath -Encoding BYTE
      }

      # Load all classes into hash
      $SCOMClasses = Get-SCOMClass
      $hashClasses = @{}
      ForEach ($Class in $SCOMClasses) {
        $hashClasses.Add($Class.Name, $Class)
      }
      
      # Load all MPs into hash. Will need the MP names below
      $MPs = Get-SCOMManagementPack
      $hashMPNames = @{}
      
      ForEach ($MP in $MPs) {
        $obj = New-Object PSCUSTOMOBJECT
        $obj | Add-Member -MemberType NoteProperty -Name IsSealed -Value $($MP.Sealed.ToString())
        If ($MP.DisplayName.Length -gt 0){
          $obj | Add-Member -MemberType NoteProperty -Name DisplayName -Value $($MP.DisplayName)
        }
        Else {
          $obj | Add-Member -MemberType NoteProperty -Name DisplayName -Value $($MP.Name)
        }
        $obj | Add-Member -MemberType NoteProperty -Name Version -Value $($MP.Version.ToString() )

        $hashMPNames.Add($MP.Name,$obj)
      }
      


      $head = @'
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title>Class Catalog</title>
    </head>
    <body>
 
<style type="text/css">
 
a { text-decoration: none; }
a:link, a:visited {
    color: blue;
}
a:hover {
    color: red;
}
 
.tg {border-collapse:collapse;border-spacing:0;}
.tg td{font-family:Arial, sans-serif;font-size:14px;padding:1px 6px;border-style:solid;border-width:1px;overflow:hidden;word-break:normal;border-color:black;}
.tg th{font-family:Arial, sans-serif;font-size:14px;font-weight:normal;padding:1px 6px;border-style:solid;border-width:1px;overflow:hidden;word-break:normal;border-color:black;}
.tg .defaultstyle{border-color:inherit;vertical-align:center;text-align:left}
.tg .headerstyle{font-weight:bold;font-size:100%;border-color:inherit;vertical-align:center}
.tg .columnNameStyle{border-color:inherit;vertical-align:center;text-align:center}
 
 
.tg .classnamestyle{font-weight:bold;border-color:inherit;vertical-align:center}
.tg .keystyle{font-weight:bold;color:#fe0000;border-color:inherit;text-align:right;vertical-align:center}
.tg .keypropertystyle{font-weight:bold;color:#fe0000;border-color:inherit;vertical-align:center}
.tg .propertystyle{color:#22771a;border-color:inherit;vertical-align:center;horizontal-align:center}
propertystyle
@media screen and (max-width: 767px)
 
{
    .tg {width: auto !important;}
    .tg col {width: auto !important;}
    .tg-wrap {overflow-x: auto;-webkit-overflow-scrolling: touch;}
}
 
#myInput {
    background-image: url('./CSS/searchicon.png'); /* Add a search icon to input */
    background-position: 10px 12px; /* Position the search icon */
    background-repeat: no-repeat; /* Do not repeat the icon image */
    width: 100%; /* Full-width */
    font-size: 16px; /* Increase font-size */
    padding: 12px 20px 12px 40px; /* Add some padding */
    border: 1px solid #ddd; /* Add a grey border */
    margin-bottom: 6px; /* Add some space below the input */
}
 
#myTable tr {
    /* Add a bottom border to all table rows */
    border-bottom: 1px solid #ddd;
}
 
#myTable tr.header, #myTable tr:hover {
    /* Add a grey background color to the table header and on hover */
    background-color: #f1f1f1;
}
 
</style>
 
<script>
function myFunction() {
  // Declare variables
  var input, filter, table, tr, td1,td2,td4,td5,td6, i;
  input = document.getElementById("myInput");
  filter = input.value.toUpperCase();
  table = document.getElementById("myTable");
  tr = table.getElementsByTagName("tr");
 
  // Loop through all table rows, and hide those who don't match the search query
  for (i = 0; i < tr.length; i++) {
    td1 = tr[i].getElementsByTagName("td")[1];
     td2 = tr[i].getElementsByTagName("td")[2];
     td4 = tr[i].getElementsByTagName("td")[4];
     td5 = tr[i].getElementsByTagName("td")[5];
     td6 = tr[i].getElementsByTagName("td")[6];
    if (td1) {
      if ( (td1.innerHTML.toUpperCase().indexOf(filter) > -1) || (td2.innerHTML.toUpperCase().indexOf(filter) > -1) || (td4.innerHTML.toUpperCase().indexOf(filter) > -1) || (td5.innerHTML.toUpperCase().indexOf(filter) > -1) || (td6.innerHTML.toUpperCase().indexOf(filter) > -1)) {
        tr[i].style.display = "";
      } else {
        tr[i].style.display = "none";
      }
    }
  }
}
</script>
 
 
<input type="text" id="myInput" onkeyup="myFunction()" placeholder="Search for names..">
 
<div class="tg-wrap">
    <table id="myTable" class="tg">
 
      <tr>
        <th class="headerstyle">#</th>
        <th class="headerstyle">Class Name</th>
        <th class="headerstyle">DisplayName</th>
        <th class="headerstyle">IsAbstract</th>
        <th class="headerstyle">MP DisplayName</th>
        <th class="headerstyle">MP Name</th>
        <th class="headerstyle">MP Version</th>
        <th class="headerstyle">MP IsSealed</th>
      </tr>
 
'@


      <# (Get-ChildItem -Path $outDir -Filter *.$($filterExt) -Exclude "*MD5Hash_*" -Recurse | `
          ForEach-Object {"<a href=`"$($_.FullName)`">$($_.Name.Replace(".$($filterExt)",''))</a></br>"}) | `
          Set-Content (Join-Path $outDir $indexFileName)
      #>

      $Rows = ''
      $thisRow = ''
      [int]$i = 0
      $graphFiles = (Get-ChildItem -Path $outDir -Filter *.$($filterExt) -Exclude "*MD5Hash_*","searchicon.png" -Recurse )
      ForEach ($file in $graphFiles) {
        $className = $file.Name.Replace(".$($filterExt)",'')
        $i++
        # $($)
        # $($hashClasses[$className].VAR)
        # <td class="defaultstyle"><a href="$($file.FullName)">$($className)</a></td>
        Try{
          $thisRow = @"
      <tr>
        <td class="defaultstyle">$($i)</td>
        <td class="defaultstyle"><a href="./$($file.Name)">$($className)</a></td>
        <td class="defaultstyle">$($hashClasses[$className].DisplayName)</td>
        <td class="defaultstyle">$($hashClasses[$className].Abstract)</td>
        <td class="defaultstyle">$($hashMPNames.($hashClasses[$className].Identifier.Domain[0]).DisplayName)</td>
        <td class="defaultstyle">$($hashClasses[$className].Identifier.Domain[0])</td>
        <td class="defaultstyle">$($hashMPNames.($hashClasses[$className].Identifier.Domain[0]).Version)</td>
        <td class="defaultstyle">$($hashMPNames.($hashClasses[$className].Identifier.Domain[0]).IsSealed)</td>
      </tr>
 
"@

        }Catch {
          Write-Error $Error[0].Exception
        }
        $Rows += $thisRow
      }

      $end += @'
</table></div>
</body></html>
'@


      ($head + $Rows + $end) | Set-Content (Join-Path $outDir $indexFileName)

      If ($ShowIndex) { 

        # Open the index file with default program
        & (Join-Path $outDir $indexFileName)
      }
    }

    #######################################################################


    #region Styling/Colors
    $c_Key = 'Red'
    $s_KeyBold = $true

    $c_classAttribute = '#7503a5'#'#8e8a86'
    $s_ClasAttributeBold = $false #$true

    $c_ExtensionFont = 'green'

    $c_defaultHdrBG = 'black'
    $c_defaultHdrFont = 'white'
    $c_defaultFill = 'white'
    $c_defaultStyle = ''
    $c_defaultTable = 'black'

    $c_DiscHdrFont = 'white'
    $c_DiscHdrBG = '#206dea' #Blue
    $c_DiscFill = ''
    $c_DiscStyle = ''
    $c_DiscTable = '#002560' #DarkBlue

    $c_DiscRow = 'blue'
    $c_DiscDisplayName = '#0646ad'
    $c_DiscTarget = '#0646ad'

    $c_hostedHdrFont = '#d87d22' #'orange' #'#e0bc60'
    $c_hostedHdrBG = 'white' #gray
    $c_hostedFill = '' #'#e0bc60'
    $c_hostedStyling = ''
    $c_hostedTable = 'green' #'orange' #'#c66d00'

    $c_hostingHdrFont = '#cc6602'
    $c_hostingHdrBG = 'white'
    $c_hostingFill = '#ffd089'#'#f7be6a'
    $c_hostingStyle = 'filled'
    $c_hostingTable = 'black' #$c_hostingFill #'#c96d12'
    $hashEdgeColor = [ordered]@{
      Child = 'black'
      Discovery = $c_DiscDisplayName
      Hosted = $c_hostingHdrFont
    }

    $c_AbstHdrFont = '#706d72' #'gray'
    $c_AbstHdrBG = '#e9ccf9' 
    $c_AbstFill = ''
    $c_AbstStyle = ''
    $c_AbstTable = '#e9ccf9'
    #endregion Styling/Colors

    # If multiple classes are meant to be combined on a single graph, the existence this variable will enable that functionality.
    If ($Combine){
      $hashGraph = @{}
    }

    #Example: C:\Users\tpaul\AppData\Local\Temp\New-SCOMClassGraph
    If (-Not [Bool]$outDir ) {
      $outDir = (Join-Path $env:Temp 'New-SCOMClassGraph')
    }
    If (-NOT(Test-Path -Path $outDir -PathType Container)) {
      New-Item -Path $outDir -ItemType Directory -Force -ErrorAction SilentlyContinue
    }

  }#end Begin


  Process{
    #$hashGraph = @{}
    $hashDiscoveries = @{}
    If ([Bool]$ID){
      $ID = $ID | ForEach-Object { $_ -Replace '{|}','' }
      $Class = (Get-SCClass -ID ($ID | Select-Object -Unique))
    }
    ElseIf ([Bool]$ClassName){
      $Class = (Get-SCClass -Name ($ClassName | Select-Object -Unique) )
      If (-Not [Bool]$Class){
        $Class = (Get-SCClass -DisplayName ($ClassName | Select-Object -Unique))
        If (-Not [Bool]($Class)) {
          Write-Host "Unable to retrieve class: $($ClassName). Check spelling." -F Yellow
          Return;
        }
      }
    }
    
    ForEach ($thisClass in $Class) {
      $HostedExists = $false
      $HostingExists = $false
      $fileNameBase = $thisClass.Name

      If (-NOT $Combine) {
        $outPath = (Join-Path $OutDir ("$fileNameBase" +".png" ))
      }
      # If the graph has already been created, no sense creating it again. Open exising file.
      If ($caching) {
        If ($Combine) {
          Write-Error "Cannot use -Combine:true -Caching:true together"
          Break
        }
        ElseIf (Test-Path -Path $outPath -PathType Leaf) {
          Invoke-Item $outPath
          Break
        }
      }


      If (-NOT $Combine){
        # This will clear the variable for every iteration, start a fresh graph of class
        $hashGraph = @{}

        #This will dig into the Class taxonomy and identify the entire family tree, storing all parents/properties/relationships into a hash table.
        $hashGraph = (Dig-Class -Class $thisClass )

        If ($ShowDiscoveries) {
          # This will clear the variable for every iteration, start a fresh graph of discoveries
          $hashDiscoveries = @{}
          #This will get any discoveries related to any of the classes
          $hashDiscoveries = Get-Discoveries -hashMaster $hashGraph 
        
          # This will stylize the Discovery nodes.
          $hashDiscoveries = Stylize-Record -hashtable $hashDiscoveries
        } 

        # This will stylize the class nodes (names, properties) based on their type (abstract, hosted, hosting, etc.)
        $hashGraph = Stylize-Record -hashtable $hashGraph
        # Will output individual graphs
        New-Graph 
      }
      Else {
        #This will dig into the Class taxonomy and identify the entire family tree, storing all parents/properties/relationships into a hash table.
        $hashGraph = (Dig-Class -Class $thisClass -hashCollection $hashGraph)
        If ($ShowDiscoveries) {
          #This will get any discoveries related to any of the classes.
          $hashDiscoveries = Get-Discoveries -hashMaster $hashGraph -hashDiscoveries $hashDiscoveries
        
          # This will stylize the Discovery nodes.
          $hashDiscoveries = Stylize-Record -hashtable $hashDiscoveries
        } 
      }
    } #end ForEach Class

  }#end Process

  End{
    
    # Will output one graph to display all classes
    If ($Combine){ 
      # This will stylize the class nodes (names, properties) based on their type (abstract, hosted, hosting, etc.)
      $hashGraph = Stylize-Record -hashtable $hashGraph
      New-Graph 
    }

    If ($ShowIndex) { 
      # Create a new index file
      New-SCOMClassGraphIndex -outDir $outDir -ShowIndex
    }
  }

}#end New-SCOMClassGraph


#######################################################################


<#
    .SYNOPSIS
    This is used to generate a unique hash string from an ordinary string value.
 
    .NOTES
    Author: Tyson Paul
    Date: 2018.05.30
    Blog: https://blogs.msdn.microsoft.com/tysonpaul/2018/07/18/scomhelper-powershell-module-a-scom-admins-best-friend/
    History:
    Adapted from http://jongurgul.com/blog/get-stringhash-get-filehash/
#>

Function Get-StringHash{
  [CmdletBinding(DefaultParameterSetName='Parameter Set 1',
      SupportsShouldProcess=$true,
      PositionalBinding=$false,
      HelpUri = 'https://blogs.msdn.microsoft.com/tysonpaul/',
  ConfirmImpact='Medium')]

  Param (
    [Parameter(Mandatory=$true,
        ValueFromPipeline=$true,
        ValueFromPipelineByPropertyName=$true,
        ValueFromRemainingArguments=$false,
        Position=0,
    ParameterSetName='Parameter Set 1')]
    [String] $String,
    $HashName = "MD5"
  )

  $StringBuilder = New-Object System.Text.StringBuilder
  [System.Security.Cryptography.HashAlgorithm]::Create($HashName).ComputeHash([System.Text.Encoding]::UTF8.GetBytes($String))|ForEach-Object{
    [Void]$StringBuilder.Append($_.ToString("x2"))
  }
  $StringBuilder.ToString()
}

#######################################################################

<#
    .SYNOPSIS
    Will export Operations Manager event log events to CSV
    .EXAMPLE
    PS C:\> Export-SCOMEventsToCSV
 
    The above example will output the newest 1000 SCOM events (default 1000) to a .CSV at C:\<computername>_OpsManEvents.csv
 
    .EXAMPLE
    PS C:\> Export-SCOMEventsToCSV -Newest 1500 -Path c:\Temp\SCOMlog.csv
 
 
    .NOTES
    Author: Tyson Paul
    Blog: https://blogs.msdn.microsoft.com/tysonpaul/2018/07/18/scomhelper-powershell-module-a-scom-admins-best-friend/
    Original Date: 2018.03.22
    History:
 
#>

Function Export-SCOMEventsToCSV {
  Param (
    [int]$Newest = 1000,
    [string]$OutFileCSV="C:\$($env:COMPUTERNAME)_OpsManEvents.csv"
  )
  Get-EventLog -LogName 'Operations Manager' -Newest $Newest | Export-Csv -Path $OutFileCSV -NoTypeInformation -Force
  Get-Item $OutFileCSV
}

#######################################################################
<#
    .Synopsis
    Will return the friendly name of a RunAs account.
    .DESCRIPTION
    Often times RunAs account SSIDs will appear in Operations Manager event logs which makes it difficult to determine which account is involved. This will correlate the friendly name with the SSID provided.
    .EXAMPLE
    PS C:\> Get-SCOMRunAsAccountName -SSID '0000F61CD9E515695ED4A018518C053E3CD87251D500000000000000000000000000000000000000'
    .Parameter -SSID
    The SSID as it is presented in the Operations Manager event log; an 80 character string of Hex digits [0-9A-F]
    .NOTES
    Author: Tyson Paul
    Blog: https://blogs.msdn.microsoft.com/tysonpaul/2018/07/18/scomhelper-powershell-module-a-scom-admins-best-friend/
    Original Date: 2013.09.23
    History: I think I originally got most of this from a Technet blog: https://social.technet.microsoft.com/Forums/systemcenter/en-US/0b9bd679-a712-435e-9a27-8b3041cddac8/how-to-find-the-runasaccount-from-the-ssid?forum=operationsmanagergeneral
#>

Function Get-SCOMRunAsAccountName{
  param (
    [Parameter(
        Mandatory=$true,
    HelpMessage="Please enter the SSID")]
    [ValidateNotNull()]
    [ValidateNotNullOrEmpty()]
    [string]$SSID
  )

  Get-SCOMRunAsAccount | Sort-Object Name | ForEach-Object {
    $string = $null;$_.SecureStorageId | ForEach-Object {
      $string = $string + "{0:X2}" -f $_
    }

    $RunAsAccountName = $_.Name
    [string]$RunAsAccountSSID = $string
    If ($SSID -match $RunAsAccountSSID) {
      Write-Host "The Run As Account Name is: $RunAsAccountName"
    }
  }
}#end Function
#######################################################################
<#
    .Synopsis
    Will display all known modules contained in all sealed management packs as well as basic schema information.
    Based on the original script found here: http://sc.scomurr.com/scom-2012-r2-mp-authoring-getting-modules-and-their-configurations/
    .EXAMPLE
    PS C:\> Show-SCOMModules
 
    .EXAMPLE
    PS C:\> Show-SCOMModules | Out-GridView
 
    .NOTES
    Author: Tyson Paul
    Blog: https://blogs.msdn.microsoft.com/tysonpaul/2018/07/18/scomhelper-powershell-module-a-scom-admins-best-friend/
    Original Date: 2017.12.13
 
    History: 2018.01.22: Refined.
    2017/12/13: First version
#>

Function Show-SCOMModules {

  $collection=@()
  $RAAP = Get-SCOMRunAsProfile
  ForEach ($MP in (Get-SCManagementPack | Where-Object{$_.Sealed -eq $True})) {
    ForEach($Module in ($MP.GetModuleTypes() | Sort-Object -Property Name | Sort-Object -Property XMLTag)) {
      $RunAs = ''
      $Object = New-Object PSObject
      $Object | Add-Member Noteproperty -Name Module -Value $Module.Name
      $Object | Add-Member Noteproperty -Name ManagementPackName -Value $MP.Name
      $Object | Add-Member Noteproperty -Name Accessibility -Value $Module.Accessibility

      If ([bool]$Module.RunAs){
        $RunAs = Get-ModuleRunasProfileName -Module $Module -Profiles $RAAP
      }
      $Object | Add-Member Noteproperty -Name RunAs -Value $RunAs
      $Object | Add-Member Noteproperty -Name ID -Value $Module.ID

      if($null -ne $Module.Configuration.Schema) {
        #
        $set = ($Module.Configuration.schema.split("<") | Where-Object {$_ -match "^xsd:element.*"} | `
        ForEach-Object -Process {$_.substring($_.indexof("name")).split("=")[1].split()[0]})
        $Object | Add-Member Noteproperty Schema $set
      }
      $Object | Add-Member Noteproperty -Name Managed -Value $Module.Managed

      $collection += $object
    }
  }

  Return $collection
}

#######################################################################
<#
    .Synopsis
    Will standardize all of the aliases in one or more unsealed (.xml) management pack files.
    .DESCRIPTION
    This script will inventory all of the MP reference IDs that can be found in the single file or set of .XML files specified by the InputPath. It will create a customized set of condensed alias acronyms to be standardized across all of the unsealed management packs. It will then replace the aliases in the Manifest as well as throughout the elements where the aliases are used.
    .Parameter InputPath
    This can be a directory or full path to an unsealed management pack .xml file.
    .EXAMPLE
    PS C:\> Set-SCOMMPAliases -InputPath 'C:\UnSealedMPs\'
    .EXAMPLE
    PS C:\> Set-SCOMMPAliases -InputPath 'C:\UnSealedMPs\MyCustom.Monitoring.xml'
 
    .NOTES
    Author: Tyson Paul
    Blog: https://blogs.msdn.microsoft.com/tysonpaul/2018/07/18/scomhelper-powershell-module-a-scom-admins-best-friend/
    Date: 2017/12/13
    History:
    2017/12/13: First version
#>

Function Set-SCOMMPAliases {
  Param (
    [Parameter(Mandatory=$true)]
    [ValidateNotNull()]
    [ValidateNotNullOrEmpty()]
    [ValidateScript({Test-Path -Path $_})]
    [string]$InputPath #Either a folder or full file path
  )

  ##### TESTING ######
  #$InputPath = 'C:\Test\Test\TempTest'
  ##### TESTING ######

  # This will be used to seed the reference catalog with some hardcoded values for some common aliases
  $seed =  @'
Microsoft.SystemCenter = SC
Microsoft.SystemCenter.Apm.Infrastructure = APMInf
Microsoft.SystemCenter.Apm.Infrastructure.Monitoring = APMInfMon
Microsoft.SystemCenter.Apm.Library = APMLib
Microsoft.SystemCenter.Apm.NTServices = APMNT
Microsoft.SystemCenter.Apm.Wcf = APMWcf
Microsoft.SystemCenter.Apm.Web = APMWeb
Microsoft.SystemCenter.DataWarehouse.ApmReports.Library = APMReports
Microsoft.SystemCenter.InstanceGroup.Library = SCIGL
Microsoft.SystemCenter.Internal = SCInt
Microsoft.SystemCenter.Library =SCLib
Microsoft.SystemCenter.WebApplication.Library = WebAppLib
Microsoft.SystemCenter.WebApplicationSolutions.Library = WebAppSolLib
Microsoft.SystemCenter.WebApplicationSolutions.Library.Resources.ENU = WebAppLibRes
Microsoft.SystemCenter.WebApplicationTest.External.Library = WebAppTestExtLib
Microsoft.SystemCenter.WebApplicationTest.Library = WebAppTestLib
Microsoft.Windows.InternetInformationServices.2003 = IIS2003
Microsoft.Windows.Library = Windows
Microsoft.Windows.Server.Library = WinSrvLib
System.Health.Library = Health
System.Library = System
System.NetworkManagement.Library = NetManLib
System.NetworkManagement.Monitoring = NetManMon
System.NetworkManagement.Reports = NetManRpt
System.NetworkManagement.Templates = NetManTmp
System.Performance.Library = Performance
'@

  # $seed -split "`r`n" | Sort-Object

  ##############################################################
  # Will replace common/popular namespaces with smaller, friendly acronym
  Function AcronymCommonNames {
    Param (
      [Parameter(Mandatory=$true)]
      [ValidateNotNull()]
      [ValidateNotNullOrEmpty()]
      [string]$thisString
    )
    $acronyms = ConvertFrom-StringData -StringData @"
Microsoft.SQLServer = MSQL
Microsoft.Windows.Server = MWS
Microsoft.Windows.InternetInformationServices = IIS
"@


    If ( $acronyms.ContainsKey($thisString) ) {
      $arrNames = $acronyms.GetEnumerator().Name
      $arrNames | ForEach-Object {
        #Write-Host $_.Name $_.value
        $thisString = $thisString.Replace(($_),($acronyms.$_) )
      }
    }
    Return $thisString
  }


  ##############################################################
  Function Create-Alias {
    Param(
      [Parameter(Mandatory=$true)]
      [ValidateNotNull()]
      [ValidateNotNullOrEmpty()]
      [string]$Seed,

      [Parameter(Mandatory=$true)]
      [ValidateNotNull()]
      [ValidateNotNullOrEmpty()]
      # array of existing values, to prevent duplicates
      [system.Object[]]$Existing
    )

    $array = AcronymCommonNames -thisString $Seed
    $array = $array.Split('.')
    $newAlias = $array[0].Replace('Microsoft','').Replace('Windows','Win').Replace('HewlettPackard','HP')
    For($i = 1; $i -lt $array.Count; $i++) {
      $fragment = $array[$i]
      If ( $fragment -match '\d+' ) {
        $newAlias += $fragment
      }
      ElseIf ( $fragment -match '^ApplicationMonitoring.?' ) {
        $newAlias += 'AppMon'
      }
      ElseIf ( $fragment -match '^Dashboard.?' ) {
        $newAlias += 'Dash'
      }
      ElseIf ( $fragment -match '^Discovery.?' ) {
        $newAlias += 'Disc'
      }
      ElseIf ( $fragment -match '^Library.?' ) {
        $newAlias += 'Lib'
      }
      ElseIf ( $fragment -match '^Linux.?' ) {
        $newAlias += 'Linux'
      }
      ElseIf ( $fragment -match '^Monitor.?' ) {
        $newAlias += 'Mon'
      }
      ElseIf ( $fragment -match '^NetworkManagement.?' ) {
        $newAlias += 'NetMan'
      }
      ElseIf ( $fragment -match '^Network.?' ) {
        $newAlias += 'Net'
      }
      ElseIf ( $fragment -match '^Report.?' ) {
        $newAlias += 'Rpt'
      }
      ElseIf ( $fragment -match '^Server.?' ) {
        $newAlias += 'Ser'
      }
      ElseIf ( $fragment -match '^System.?' ) {
        $newAlias += 'Sys'
      }
      ElseIf ( $fragment -match '^Template.?' ) {
        $newAlias += 'Tmp'
      }
      ElseIf ( $fragment -match '^Unix.?' ) {
        $newAlias += 'Unix'
      }
      ElseIf ( $fragment -match '^Visual.?' ) {
        $newAlias += 'Vis'
      }
      ElseIf ( $fragment -match '^Windows.?' ) {
        $newAlias += 'Win'
      }
      Else {
        # Use capitalized letters in MP name to build the acronym
        $tempchar = ''
        [char[]]$fragment | ForEach-Object {
          If ([char]::IsUpper($_) ){$tempchar+=$_ }
        }
        # $newAlias += $fragment[0]
        $newAlias += $tempchar
      }
    }
    $i = 2
    $tempAlias = $newAlias
    While ($Existing -contains ($tempAlias)) {
      $tempAlias = $newAlias +"$i"
      $i++
    }

    Return $tempAlias
  }

  ##############################################################
  # This function will replace all instances of existing aliases with the newly customized aliases.
  Function Standardize-AllReferenceAliases {
    Param(
      [Parameter(Mandatory=$true)]
      [ValidateNotNull()]
      [ValidateNotNullOrEmpty()]
      [System.Object[]]$MPPaths,

      [Parameter(Mandatory=$true)]
      [ValidateNotNull()]
      [ValidateNotNullOrEmpty()]
      [Hashtable]$Catalog
    )
    ForEach ($MPPath in $MPPaths){

      $mpxml = [xml](Get-Content $MPPath)
      $arrReferences = $mpxml.GetEnumerator().Manifest.references.reference
      [int]$i = 0
      $content = (Get-Content $MPPath)
      # Replace aliases wherever used in xml
      ForEach ($ref in ($arrReferences) ) {
        <# Aliases typically appear in two scenarios...
            1) Examples: referring to some element property:
            <Property>$MPElement[Name="Windows!Microsoft.Windows.Computer"]/PrincipalName$</Property>
            <MonitoringClass>$MPElement[Name="SCLib!Microsoft.SystemCenter.ManagedComputer"]$</MonitoringClass>
            <RelationshipClass>$MPElement[Name="SCIGL!Microsoft.SystemCenter.InstanceGroupContainsEntities"]$
        #>

        $aliasrefbang = '="'+"$($ref.Alias)"+"!"
        $newAlias = ('="'+($Catalog.($ref.ID))+'!')
        $content = ($content -Replace ([regex]::Escape($aliasrefbang)), ([regex]::Escape($newAlias)) )

        <#
            2) Examples: referring to a workflow, type, datasource, etc.:
            <DataSource ID="GroupPopulationDataSource" TypeID="SCLib!Microsoft.SystemCenter.GroupPopulator">
            <DiscoveryRelationship TypeID="SCIGL!Microsoft.SystemCenter.InstanceGroupContainsEntities" />
        #>

        $aliasrefbang = '>'+"$($ref.Alias)"+"!"
        $newAlias = ('>'+($Catalog.($ref.ID))+'!')
        $content = ($content -Replace ([regex]::Escape($aliasrefbang)), ([regex]::Escape($newAlias)) )
        #$content | Set-Content $MPPath -Force -Encoding UTF8
      }

      # Reload the XML because it was just rewritten
      #$mpxml = [xml](Get-Content $MPPath)
      $mpxml = [xml]($content)
      $arrReferences = $mpxml.GetEnumerator().Manifest.references.reference
      [int]$i = 0
      # Replace aliases in Manifest
      ForEach ($ref in ($arrReferences) ) {
        $ref.Alias = $Catalog.($ref.ID)
        $i++
      }

      Write-Host "`t$i references updated! [ " -NoNewline -b Black -f Cyan
      Write-Host $(Split-Path -Path $MPPath -Leaf) -NoNewline
      Write-Host " ]" -b Black -f Cyan
      $mpxml.Save($MPPath)
    }
  } #endFunction

  ##############################################################
  <# This function is designed to append (or trim) a unique hash value to/from the aliases
      (in the Manifest as well as elsewhere in the management pack). This is necessary in the rare cases where existing
      aliases already match my customized new condensed acronyms in the $cat (catalog). "Windows!" is a good example
      of this. I've encountered random MPs which have the alias, "Windows" already assigned to a reference. This becomes
      problematic when I attempt to change references to the 'Microsoft.Windows.Library' after which the file ends up with
      multiple identical alias references (to different sealed MPs) using the exact same alias, "Windows". This is a clever way
      to make sure that all aliases are unique (no conflicts). This function should be run twice on the catalog; Once to
      append the unique string to aliases in the catalog (after the cat has been generated, of course), then modify the files.
      Then run again to set aliases to their final, desired values, then modify the files again to their final desired values.
  #>

  Function Modify-Aliases {
    Param(
      [Hashtable]$cat,
      [Switch]$Localized
    )
    $hash = Get-StringHash $env:COMPUTERNAME
    If ($Localized){
      @($cat.Keys) | ForEach-Object {
        $Cat[$_] = $cat[$_]+$hash
      }
    }
    Else{
      @($cat.Keys) | ForEach-Object {
        $Cat[$_] = [string]$cat[$_] -Replace $hash, ""
      }
    }
    Return $cat
  }

  ##############################################################
  #http://jongurgul.com/blog/get-stringhash-get-filehash/
  Function Get-StringHash
  {
    
    param
    (
      [string]
      $String,

      [string]
      $HashName = "MD5"
    )
    $StringBuilder = New-Object System.Text.StringBuilder
    [System.Security.Cryptography.HashAlgorithm]::Create($HashName).ComputeHash([System.Text.Encoding]::UTF8.GetBytes($String))|ForEach-Object{
      [Void]$StringBuilder.Append($_.ToString("x2"))
    }
    $StringBuilder.ToString()
  }

  #-------------------------------------------------------------
  #################### MAIN ################################


  ##############################################################
  # Create catalog of all existing references/aliases
  # The idea here is to seed the catalog with common pack names.
  # For other MPs dynamically create aliases based on doted name structure
  ##############################################################
  $cat = ConvertFrom-StringData -StringData $seed

  If (Test-Path -Path $InputPath -PathType Container -ErrorAction SilentlyContinue) {
    $MPPaths = (Get-ChildItem $InputPath -Recurse -Include *.xml -File ).FullName | Sort-Object
    Write-Host "$($MPPaths.Count) files found..." -F Yellow -B Black
  }
  ElseIf ((Test-Path -Path $InputPath -PathType Leaf -ErrorAction SilentlyContinue) -and ($InputPath -like "*.xml") ) {
    $MPPaths = $InputPath
  }
  Else { Write-Host "Something is wrong with `$InputPath: [$($InputPath)]."; Exit}

  $refIDs = @()
  [int]$i=0
  ForEach ($MPPath in $MPPaths) {
    Write-Host $MPPath -F Green
    $mpxml = [xml](Get-Content $MPPath)
    #$mpxml.SelectNodes("//Reference") | % {
    $mpxml.ManagementPack.Manifest.References.Reference | ForEach-Object {
      $refIDs += $_.ID
      $_
      Write-Progress -Activity "Creating master catalog of known References" -Status $_.ID -PercentComplete ($i / $MPPaths.Count*100)
    }
    $i++
  }
  # Create array of all unique IDs
  $AllRefIDs = ($refIDs | Group-Object ).Name | Sort-Object
  $AllRefIDs | ForEach-Object {
    #If ID not in catalog yet, add it with customized alias
    If ( -not($cat.ContainsKey($_.ToString())) ){
      $cat.Add($_,(Create-Alias -Seed $_ -Existing $cat.Values) )
    }
  }

  # Update all aliases to unique values to prevent naming conflicts when modifying files
  $cat = Modify-Aliases -Localized:$true -cat $cat
  Write-Host "Updating references...(first pass, randomized unique values)" -b Black -f Yellow
  Standardize-AllReferenceAliases -MPPaths $MPPaths -Catalog $cat

  # Update all aliases to final, correct values, and modify files
  $cat = Modify-Aliases -Localized:$false -cat $cat
  Write-Host "Updating references...(final pass)" -b Black -f Green
  Standardize-AllReferenceAliases -MPPaths $MPPaths -Catalog $cat

  # Display the reference/alias Catalog
  $arrCat = @()
  ForEach ($key in $cat.Keys ){
    #Write-Host $key " = " $cat[$key]
    $arrCat += ("$key = $($cat[$key])" )
  }
  Return ($arrCat | Sort-Object)

} #End Function
#######################################################################

<#
    .Synopsis
    Will test any number of TCP ports on any number of hosts.
    .DESCRIPTION
    Will accept piped Computers/IPs or Ports and then test those ports on the targets for TCP connectivity.
    .Parameter Computer
    Computer NetBIOS name, FQDN, or IP
    .Parameter Port
    TCP Port number to test.
    .Parameter TimeoutMS
    The amount of time to wait (in milliseconds) before abandoning a connection attempt on a given port.
    .EXAMPLE
    PS C:\> Test-Port -Computer '8.8.8.8' -Port 53
    .EXAMPLE
    PS C:\> 443,80,53,135,137,5723 | Test-Port -Computer 'MS01.contoso.com','DB01' | Sort-Object Computer,Port
    .EXAMPLE
    PS C:\> 'MS01.contoso.com','DB01' | Test-Port -Port 5723
    .NOTES
    Author: Tyson Paul
    Blog: https://blogs.msdn.microsoft.com/tysonpaul/2018/07/18/scomhelper-powershell-module-a-scom-admins-best-friend/
    Version: 1.3
    Date: 2018.02.07
    History:
    2018.05.31: Improved error handling.
 
    Adapted from Boe Prox (https://gallery.technet.microsoft.com/scriptcenter/97119ed6-6fb2-446d-98d8-32d823867131)
#>

Function Test-Port {
  [CmdletBinding(DefaultParameterSetName='Parameter Set 1',
      SupportsShouldProcess=$true,
      PositionalBinding=$false,
      HelpUri = 'https://blogs.msdn.microsoft.com/tysonpaul/',
  ConfirmImpact='Medium')]
  Param (
    [Parameter(Mandatory=$true,
        ValueFromPipeline=$true,
        ValueFromRemainingArguments=$false,
        Position=0,
    ParameterSetName='Parameter Set 1')]
    [string[]]$Computer,

    [Parameter(Mandatory=$true,
        ValueFromPipeline=$true,
        ValueFromRemainingArguments=$false,
        Position=1,
    ParameterSetName='Parameter Set 1')]
    [int[]]$Port,

    [int]$TimeoutMS=5000
  )
  Begin {
    $arrResults = @()
    $Error.Clear()
  }
  Process {
    If (-not $Port) { $Port = Read-host "Enter the port number to access" }

    ForEach ($thisComputer in $Computer) {
      ForEach ($thisPort in $Port) {
        $Status = "Failure"
        $objResult = New-Object -TypeName PSCustomObject
        $tcpobject = New-Object System.Net.Sockets.TcpClient
        #Connect to remote machine's port
        $connect = $tcpobject.BeginConnect($thisComputer,$thisPort,$null,$null)
        #Configure a timeout before quitting - time in milliseconds
        $wait = $connect.AsyncWaitHandle.WaitOne($TimeoutMS,$false)
        If (-Not $Wait) {
          $Message = "Connection Failure. Address:[$($thisComputer)], Port:[$($thisPort)] connection timed out [$($TimeoutMS) milliseconds].`n"
        }
        Else {
          Try{
            $tcpobject.EndConnect($connect)  #| out-Null
            $Status = "Success"
            $Message = "Connection Success. Address:[$($thisComputer)], Port:[$($thisPort)] connection successful.`n"
          }Catch{
            #If ([bool]$Error[0]) {
            $Message = ("{0}" -f $error[0].Exception.InnerException)
            #}
            #Else {
            # $Status = "Success"
            # $Message = "Connection Success. Address:[$($thisComputer)], Port:[$($thisPort)] connection successful.`n"
            #}
          }
        }
        $objResult | Add-Member -MemberType NoteProperty -Name Computer -Value $thisComputer
        $objResult | Add-Member -MemberType NoteProperty -Name Port -Value $thisPort
        $objResult | Add-Member -MemberType NoteProperty -Name Status -Value $Status
        $objResult | Add-Member -MemberType NoteProperty -Name Result -Value $Message
        $arrResults += $objResult
        $Error.Clear()
      }#End ForEach Port
    }#End ForEach Computer
  }#End Process

  End {
    Return $arrResults
  }

} #End Function

#######################################################################
<#
    .Synopsis
    This function is designed to launch a SCOM Property Bag PowerShell script. It will output the DataItem to an xml file for inspection.
    .DESCRIPTION
    When authoring and troubleshooting a SCOM Property Bag PowerShell script it is helpful to be able to analyze the resulting DataItem. This script will make this process easier by outputing the DataItem(s) to an XML file. The default output file will have the same path/name as the input file (+.xml). Script must propertly use 'MOM.ScriptAPI' and output the proerty bag.
    Why is this useful? It's easy to output the DataItem to the screen but then it's difficult to read/analyze. If you open the .xml file with a fancy text editor like Notepad++ you can leverage the XMLTools plugin to nicely format the data. This is especially helpful for large property bags.
    Example :
    $api = New-Object -ComObject 'MOM.ScriptAPI'
    $bag = $api.CreatePropertyBag()
    $bag.AddValue('Message',"Some message")
    $api.Return($bag) #This will output the DataItem for analysis
 
    .EXAMPLE
    Show-SCOMPropertyBag -FilePath C:\test\MyScript.ps1
    This will execute C:\test\MyScript.ps1 and output the property bag data of each bag ($Bag assumed to be in the script) to <$env:windir\Temp\><script file name>_<uniquetimestamp>.xml. Individual bags will be returned in a formatted table.
 
    .EXAMPLE
    Show-SCOMPropertyBag -FilePath C:\test\MyScript.ps1 -ShowXML
    This will execute MyScript.ps1 and output the property bag data of each bag ($Bag assumed to be in the script) to <$env:windir\Temp\><script file name>_<uniquetimestamp>.xml and then open the .xml file with the default application.
 
    .EXAMPLE
    Get-ChildItem -Path C:\TestScripts\*.ps1 | Show-SCOMPropertyBag -ShowXML -Format Separate
    This will execute all PowerShell scripts in the path and output the property bag data of each bag ($Bag assumed to be in the script) to <$env:windir\Temp\><script file name>_<uniquetimestamp>.xml and then open the .xml files with the default application.
    Each property bag will be displayed individually to the screen.
 
    .EXAMPLE
    Show-SCOMPropertyBag -FilePath C:\test\MyScript.ps1 -Format Combined -Verbose
    This will execute C:\test\MyScript.ps1 and output the property bag data of each bag ($Bag assumed to be in the script) to <$env:windir\Temp\><script file name>_<uniquetimestamp>.xml.
    All property bag data will be displayed to the screen in one formatted table.
 
    Use -Verbose switch to display output file location.
 
    .NOTES
    Name: Show-SCOMPropertyBag
    Author: Tyson Paul
    History: 2019.04.19 - Initial release.
#>

Function Show-SCOMPropertyBag {
  [CmdletBinding(DefaultParameterSetName='Parameter Set 1', 
      SupportsShouldProcess=$true, 
  PositionalBinding=$false)]

  Param (
    # The full path to the input script (PowerShell .ps1) file.
    [Parameter(Mandatory=$false, 
        ValueFromPipeline=$true,
        ValueFromPipelineByPropertyName=$true, 
        ValueFromRemainingArguments=$false, 
        Position=0,
    ParameterSetName='Parameter Set 1')]
    [ValidateNotNull()]
    [ValidateNotNullOrEmpty()]
    [string]$FilePath,

    # Powershell script file object. Typically this type of object would be obtained with Get-ChildItem.
    [Parameter(Mandatory=$false, 
        ValueFromPipeline=$true,
        ValueFromPipelineByPropertyName=$true, 
        ValueFromRemainingArguments=$false, 
        Position=0,
    ParameterSetName='Parameter Set 2')]
    [ValidateNotNull()]
    [ValidateNotNullOrEmpty()]
    [System.IO.FileInfo]$File,

    [Parameter(Mandatory=$false, 
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false, 
        ValueFromRemainingArguments=$true, 
    Position=1)]
    [ValidateSet("Combined", "Separate","HashTable")]
    #Output formated objects (bags) instead of default hash table.
    # Separate: Individual bags
    # Combined: One large table containing all data from all bags
    # HashTable: Hash table containing all bag data. Useful for spying on data values that don't otherwise display in the table format due to large size.
    [string]$Format='Separate',

        
    # This will open the .XML output file type with default application.
    [switch]$ShowXML
  )
  #region BEGIN
  Begin{
    #----------------- FUNCTIONS -----------------------
    Function FormatEach {
      Param(
        $hash 
      )

      Return $hash.Value
      
    }
    
    #--------------- END FUNCTIONS ---------------------

    $hashVariantTypes = [ordered]@{
      0 = "Empty"
      1 = "Null (unconfirmed)"
      2 = "Short (unconfirmed)"
      3 = "Integer"
      4 = "Float"
      5 = "Double"
      6 = "Currency (unconfirmed)"
      7 = "Date"
      8 = "String"
      9 = "Object (unconfirmed)"
      10 = "Error (unconfirmed)"
      11 = "Boolean"
      12 = "Variant (unconfirmed)"
      13 = "DataObject (unconfirmed)"
      14 = "Decimal"
      15 = "Byte (unconfirmed)"
      16 = "Char (unconfirmed)"
      17 = "Byte"
      18 = "Char"
      20 = "Long"
    }

    # Setup temp dir for output file. Using a redirector to an output file is the only way I could find to obtain the property bag data.
    $TempDir = (Join-Path (Join-Path $env:Windir "Temp") "SCOMHelper" )
    New-Item -Type Directory -Path $TempDir -ErrorAction SilentlyContinue | Out-Null
    If (-not(Test-Path -Path $TempDir -PathType Container)) {
      Write-Error "Unable to create/access directory: [$($TempDir)]. " 
      Return $false
    }
    $hash = @{}
    $int=1
  }
  #endregion BEGIN
  
  
  Process {
    $batchstamp = Get-Date -Format o | ForEach-Object {$_ -replace ":", "."}
    If ([bool]$File){
      If (Test-Path -PathType Leaf -Path $File.FullName -ErrorAction SilentlyContinue){
        $OutFile = (Join-Path $TempDir "$($File.Name)_$($batchstamp)_.xml" )
        cmd /c PowerShell.exe -file $File.FullName > $OutFile
      }
    }

    Else {
      If (-NOT [bool]$FilePath) {
        #$FilePath = 'C:\Program Files\WindowsPowerShell\Modules\SCOMHelper\1.1\SCOMPowerShellPropertyBagTypes.ps1'
        $FilePath = Join-Path $psScriptRoot SCOMPowerShellPropertyBagTypes.ps1
      }
      If (Test-Path -PathType Leaf -Path $FilePath -ErrorAction SilentlyContinue){
        $OutFile = (Join-Path $TempDir "$(Split-Path $FilePath -Leaf)_$($batchstamp)_.xml")
        cmd /c PowerShell.exe -file $FilePath > $OutFile
      }
      Else {
        Throw "Problem with FilePath: [ $($FilePath) ]."
      }
    }
    # Remove generic typename that often appears as a result of standard '$bag' statement
    $content = ((Get-Content $OutFile -Raw ).Replace('System.__ComObject','').Replace('ÿ','')) 
    $content | Set-Content $OutFile
    
    $token = (Get-Date -f fffffff)
    # Add a delimiter after each DataItem, then split on it.
    $array = (($content -replace '\<\/DataItem\>',"</DataItem>$($token)" ) -split $token)

    ForEach ($DI in $array[0..($array.Count -2)]){
      $arrayItems = @()
      'type','time','sourceHealthServiceId' | ForEach-Object {
        $obj = New-Object PSCUSTOMOBJECT

        $obj | Add-Member -MemberType NoteProperty -Name 'Name' -Value $_
        $obj | Add-Member -MemberType NoteProperty -Name 'VariantType' -Value ''
        $obj | Add-Member -MemberType NoteProperty -Name 'Value' -Value (([xml]$DI).DataItem.$_)
        $arrayItems += $obj
      }
 
      ForEach ($Property in ([xml]$DI).DataItem.Property){
        $obj = New-Object PSCUSTOMOBJECT
        $obj | Add-Member -MemberType NoteProperty -Name "Name" -Value ([string]$Property.Name)
        $obj | Add-Member -MemberType NoteProperty -Name "VariantType" -Value ("{0,2},{1}" -F $([int]$Property.VariantType), $($hashVariantTypes.[int]$Property.VariantType))
        $obj | Add-Member -MemberType NoteProperty -Name "Value" -Value ([string]$Property.'#text')
        $arrayItems += $obj
      }
      $hash.Add($int,$arrayItems)
      $int++
    }
    
    If ($ShowXML) {
      # Open with default application
      & $OutFile
    }

    Write-Verbose "Output file: $OutFile"
  }

  End {
    If ($Format -eq 'Separate'){
      ForEach ($element in @($hash.GetEnumerator() )) { 
        FormatEach -hash $element | Out-Host
      }
    }
    
    ElseIf ($Format -eq 'Combined'){
      ForEach ($element in @($hash.GetEnumerator() )) { 
        FormatEach -hash $element 
      }
    }
    
    Else{
      Return $hash
    }
  }
} #end Function




#######################################################################

<#
    .SYNOPSIS
    Operations Manager Powershell script to output the effective monitoring configurations for a specified object ID, group, or Computer name.
    .DESCRIPTION
    Will recursively find contained instances of an object (or group of objects) and output the effective monitoring configurations/settings to individual output files.
    Then will merge all resulting files into one .csv file, delimited by pipes '|', then output all data to Grid View.
    .EXAMPLE
    PS C:\> Export-EffectiveMonitoringConfiguration -SCOMGroupName "All Windows Computers" -TargetFolder "C:\Export\MyConfigFiles" -OutputFileName "AllWindowsComputers_MonitoringConfigs.CSV"
    .EXAMPLE
    PS C:\> Export-EffectiveMonitoringConfiguration -ComputerName "SQL01.contoso.com" -TargetFolder "C:\Export" -OutputFileName "Output.csv"
    .EXAMPLE
    The following example returns the Monitoring object ID of a Windows Computer instance with name: "db01.contoso.com". The ID is then used as a parameter.
 
    PS C:\> $ComputerID = (Get-SCOMClass -name "Microsoft.windows.computer" | Get-SCOMClassInstance | ? {$_.DisplayName -like "db01.contoso.com"}).Id
    PS C:\> Export-EffectiveMonitoringConfiguration -ID $ComputerID -TargetFolder "C:\Export" -OutputFileName "Output.csv"
    .EXAMPLE
    The following example will output all monitoring configuration info for a specific computer to a csv file. There will be no confirmation prompt to proceed.
    Any previously stored DisplayName file will be removed and recreated. This will increase run time of script as it will have to retrieve all of the workflow Displaynames and
    this is very expensive on the SQL database. It will then display that information in GridView. The -PassThru parameter
    will allow the user to filter the view and then export any selected GridView line items to a new csv file named "FilteredOutput.csv"
 
    PS C:\> Export-EffectiveMonitoringConfiguration -ComputerName "win01.contoso.com" -TargetFolder "C:\Export" -OutputFileName "WIN01_Output.csv" -NoConfirm -ClearCache -NoGridView
    PS C:\> Import-Csv 'C:\Export\Merged_WIN01_Output.csv' -Delimiter '|' | Out-GridView -PassThru | Export-Csv -Path 'C:\Export\FilteredOutput.csv' -NoTypeInformation -Force
 
    .NOTES
    Author : Author: Tyson Paul
    Blog: : https://blogs.msdn.microsoft.com/tysonpaul/2018/07/18/scomhelper-powershell-module-a-scom-admins-best-friend/
    Requires : Operations Manager Powershell Console
    Version : 1.16
    Original Date: 7-25-2014
    History
    2018.09.14: Modified 'ID' and 'ComputerName' parameters to allow arrays.
    Added 'Path' column.
    2018.04.06: Added column to output: Rule/Monitor DisplayName
    2017.11.28: Fixed paramter types.
    2017.10.11: Added ability to specify object ID or single computer name. Improved Help content. Improved output formatting.
    2014.8.6: Fixed output file name/path problem.
    .PARAMETER -ComputerName
    Accepts a single FQDN (Fully Qualified Domain Name) name of an monitored computer.
    .PARAMETER -ID
    Accepts a single guid. Guid should contain 32 digits with 4 dashes (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx).
    .PARAMETER -SCOMGroupName
    Accepts a single group name.
    .PARAMETER -TargetFolder
    The full path to where the script should output the configuration files.
    .PARAMETER -OutputFileName
    The name of the complete output file in which all other configuration files will be compiled.
    .PARAMETER -NoConfirm
    Switch. Will not prompt user for confirmation to proceed. (Caution should be taken when targeting large groups.)
    .PARAMETER -NoGridView
    Switch. Will not display configuration in PowerShell GridView
    .PARAMETER -ClearCache
    Switch. Will delete any existing DisplayName file. This is a file that is saved in the $ENV:TEMP path. It will save a list of
    workflow Names and DisplayNames from previous executions of this script. This will significantly speed up the runtime of the script.
    This option is only useful if management packs have been updated and DisplayNames of workflows have been modified since the script
    was last run. When enabled, this parameter will force the script to retrieve all of the DisplayNames from the SDK instead of the locally stored file.
 
#>

Function Export-EffectiveMonitoringConfiguration {
  [CmdletBinding(DefaultParameterSetName='P1',
      SupportsShouldProcess=$true,
  PositionalBinding=$false)]

  Param
  (
    # 1
    [Parameter(Mandatory=$true,
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false,
        ValueFromRemainingArguments=$false,
        Position=0,
    ParameterSetName='P1')]
    [ValidateNotNull()]
    [ValidateNotNullOrEmpty()]
    [string[]]$ComputerName,

    #2
    [Parameter(Mandatory=$true,
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false,
        ValueFromRemainingArguments=$false,
        Position=0,
    ParameterSetName='P2')]
    [ValidateNotNull()]
    [ValidateNotNullOrEmpty()]
    [string]$SCOMGroupName,

    #3
    [Parameter(Mandatory=$true,
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false,
        ValueFromRemainingArguments=$false,
        Position=0,
    ParameterSetName='P3')]
    [ValidateNotNull()]
    [ValidateNotNullOrEmpty()]
    # Validate GUID pattern
    [ValidatePattern("[a-f0-9][a-f0-9][a-f0-9][a-f0-9][a-f0-9][a-f0-9][a-f0-9][a-f0-9]-[a-f0-9][a-f0-9][a-f0-9][a-f0-9]-[a-f0-9][a-f0-9][a-f0-9][a-f0-9]-[a-f0-9][a-f0-9][a-f0-9][a-f0-9]-[a-f0-9][a-f0-9][a-f0-9][a-f0-9][a-f0-9][a-f0-9][a-f0-9][a-f0-9][a-f0-9][a-f0-9][a-f0-9][a-f0-9]")]
    [System.Guid[]]$ID,

    #4
    [Parameter(Mandatory=$false,
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false,
        ValueFromRemainingArguments=$false,
    Position=1)]
    [string]$TargetFolder = "C:\SCOM_Export",

    #5
    [Parameter(Mandatory=$false,
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false,
        ValueFromRemainingArguments=$false,
    Position=2 )]
    [string]$OutputFileName,

    #6
    [Parameter(Mandatory=$false,
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false,
        ValueFromRemainingArguments=$false,
    Position=3 )]
    [switch]$NoConfirm,

    #6
    [Parameter(Mandatory=$false,
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false,
        ValueFromRemainingArguments=$false,
    Position=4 )]
    [switch]$NoGridview=$false,

    #7
    [Parameter(Mandatory=$false,
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false,
        ValueFromRemainingArguments=$false,
    Position=5 )]
    [switch]$ClearCache=$false

  )

  New-Variable -Name StartupVariables -Force -Value (Get-Variable -Scope Global | Select-Object -ExpandProperty Name)

  #####################################################################################################
  Function Cleanup(){
    $ErrorActionPreference = "SilentlyContinue"    #Depending on when this is called, some variables may not be initialized and clearing could throw benign error. Supress.
    Write-Host "`nPerforming cleanup..." -ForegroundColor Cyan
    #Cleanup
    Get-Variable | Where-Object { $StartupVariables -notcontains $_.Name } | ForEach-Object { Remove-Variable -Name "$($_.Name)" -Force -Scope 1 }
  }

  ########################################################################################################
  # Will clean up names/strings with special characters (like URLs and Network paths)
  Function CleanName {
    Param(
      [string]$uglyString
    )
    # Remove problematic characters and leading/trailing spaces
    $prettyString = (($uglyString.Replace(':','_')).Replace('/','_')).Replace('\','_').Trim()

    # If the string has been modified, output that info
    If ($uglyString -ne $prettyString) {
      Write-Verbose "There was a small problem with the characters in this parameter: [$($uglyString)]..."
      Write-Verbose "Original Name:`t`t$($uglyString)"
      Write-Verbose "Modified Name:`t`t$($prettyString)"
    }

    Return $prettyString
    #>
  }
  ########################################################################################################
  # Function MergeFiles
  # Will find .csv files and merge them together.
  Function MergeFiles{
    Param(
      [string]$strPath,
      [string]$strOutputFileName
    )

    $strOutputFilePath = (Join-Path $strPath $strOutputFileName)
    # If output file already exists, remove it.
    If (Test-Path $strOutputFilePath -PathType Leaf) {
      Write-Verbose "Output file [ $($strOutputFilePath) ] already exists. Removing..."
      Remove-Item -Path $strOutputFilePath -Force
    }

    If (Test-Path $strOutputFilePath) {
      Write-Error "Cannot remove $strOutputFilePath and therefore cannot generate merged output file." -ForegroundColor Yellow -BackgroundColor Black
      Write-Error "Remove this file first: [ $($strOutputFilePath) ]"
      Write-Error "Exiting ! "
      Cleanup
      Exit
    }

    Get-ChildItem -Path $strPath -File -Filter *.csv | Where-Object {$_.Name -notlike (Split-Path -Leaf $strOutputFileName )} | ForEach-Object {
      $intThisHeader = (Get-Content -LiteralPath $_.FullName)[0]
      If ($intThisHeader.Length -gt $intLongestHeaderLength) {
        $objLongestHeaderFile = $_
        $intLongestHeaderLength = $intThisHeader.Length
      }
    }
    
    Write-Host "Has largest set of file headers: [ $($objLongestHeaderFile.FullName) ] "
    # Create the master merge file seeded by the data from the existing CSV file with the most headers out of the entire set of CSVs.
    Try{
      Get-Content $objLongestHeaderFile.FullName | Out-File -LiteralPath $strOutputFilePath -Force -Encoding UTF8
    }Catch{
      Write-Error $error[0]
      Write-Host "Something is wrong with this path [$($strOutputFilePath)]." -ForegroundColor Red -BackgroundColor Yellow
      Write-Host "Exiting..."
      Exit
    }
    # Iterate through all of the CSVs, append all of them into the master (except for the one already used as the seed above and except for the master merge file itself.)
    $i=0
    $tempArray = @()
    # Get-ChildItem -Path $strPath -File -Filter *.csv -Exclude "merged*" -Recurse | ForEach-Object {
    Get-ChildItem -Path $strPath -File -Filter *.csv | Where-Object {$_.Name -notlike "merged*"} | ForEach-Object {
      If( ( $_.FullName -eq $objLongestHeaderFile.FullName ) -or ($_.FullName -like (Get-Item $strOutputFilePath).FullName) ){
        Write-Host "Skip this file: `t" -NoNewline; Write-Host "$($_.FullName)" -ForegroundColor Red -BackgroundColor Yellow
      }
      Else {
        Write-Host "Merge this file: `t" -NoNewline; Write-Host "$($_.FullName)" -BackgroundColor Green -ForegroundColor Black
        # This is a complicated operation directly below. It's necessary to remove double \r\n and then remove \n, then split on the \r so that no \r\n appear in the array.
        # Also, make sure the line has a length and is not empty.
        $tempArray += (((((Get-Content -Raw -Path $_.FullName) -Replace '\r\n\r\n',"" ) -Replace "\n","") -Split "\r") | Select-Object -Skip 1 )  | Where-Object {$_.Length -gt 0} 
        $i++
      }
    }
    $tempArray | Out-File -LiteralPath $strOutputFilePath -Append -Encoding UTF8
    "" # Cheap formatting
    Write-Host "Total files merged: `t" -NoNewline; Write-Host "$i" -BackgroundColor Black -ForegroundColor Green
    Write-Host "Master output file: `t" -NoNewline; Write-Host "$strOutputFilePath" -BackgroundColor black -ForegroundColor Green
  } # EndFunction
  ###################################################################################################

  Function MakeObject {
    Param(
      [string]$strMergedFilePath
    )
    $mainArray = @()
    [string]$rootFolder = Split-Path $strMergedFilePath -Parent

    $tmpFileName = "ExportEffectiveMonitoringConfiguration.ps1_DisplayNamesCSV.tmp"
    Try {
      [string]$savedDNs = (Join-Path $env:Temp $tmpFileName )
      New-Item -ItemType File -Path $savedDNs -ErrorAction SilentlyContinue
    }Catch{
      [string]$savedDNs = (Join-Path $rootFolder $tmpFileName )
    }
    If ($ClearCache){
      Write-Host "Removing saved DisplayNames file: [$($savedDNs)]" -F Gray
      Remove-Item -Path $savedDNs -Force
    }

    If (!($strMergedFilePath)) {
      Write-Host "Cannot find [ $($strMergedFilePath) ] and therefore cannot compile object for Grid View." -ForegroundColor Yellow -BackgroundColor Black
      Write-Host "Exiting..."
      Cleanup
      Exit
    }

    $Headers = @()
    $Headers= (Get-Content -LiteralPath $strMergedFilePath | Select-Object -First 1).Split('|')
    #Get the meat and potatoes without any empty lines. (Sometimes there are empty lines in the export files)
    $FileContents = (Get-Content -LiteralPath $strMergedFilePath | Select-Object -Skip 1 | Where-Object {$_ -notmatch "^$" }).Replace("`0",'')
    $r=1

    <# The Export-SCOMEffectiveMonitoringConfiguration cmdlet does not include DisplayName in it's default output set.
        Querying the SDK for the workflow DisplayName is expensive. In the code below we try to benefit from a saved
        list of Name->DisplayName pairs. If the list does not already exist, we will create one. If it does already
        exist, we will import it into a hash table for fast DisplayName lookup while building the rows of the master file.
    #>

    $DNHash = @{}
    Try {
      [System.Object[]]$arrDN = (Import-Csv -Path $savedDNs -ErrorAction SilentlyContinue)
    } Catch {
      $arrDN = @()
    }
    # If a previous list of Name/DisplayName pairs exists, let's use it to build our fast hash table.
    If ([bool]$arrDN ){
      ForEach ($item in $arrDN) {
        $DNHash.Add($item.'Rule/Monitor Name',$item.'Rule/Monitor DisplayName')
      }
    }
    $arrTmpDN = @()
    :nextRow  ForEach ($Row in $FileContents) {
      $percent = [math]::Round(($r / $FileContents.count*100),0)
      Write-Progress -Activity "** What's happening? **" -status "Formatting your data! [Percent: $($percent)]" -percentComplete $percent
      If ($Row.Length -le 1) { Continue; }
      $c=0
      $arrRow = @()
      $arrRow = $Row.Split('|')

      # If the ForEach has already executed one iteration and thus the full object template has already been created,
      # duplicate the template instead of building a new object and adding members to it for each column. This is about 3x faster than building the object every iteration.
      If ([bool]($templateObject)) {
        $object = $templateObject.PsObject.Copy()
        $object.Index = $r.ToString("0000")
      }
      Else {
        $object = New-Object -TypeName PSObject
        $object | Add-Member -MemberType NoteProperty -Name "Index" -Value $r.ToString("0000")
      }
      ForEach ($Column in $Headers) {
        If ( ($arrRow[$c] -eq '') -or ($arrRow[$c] -eq ' ') ) { $arrRow[$c] = 'N/A' }
        # Some header values repeat. If header already exists, give it a unique name
        [int]$Position=1
        $tempColumn = $Column
        # The first 11 columns are unique. However, beyond 11, the column names repeat:
        # Parameter Name, Default Value, Effective Value
        # A clever way to assign each set of repeats a unique name is to append an incremental instance number.
        # Each set (of 3 column names) gets an occurance number provided by the clever math below.
        # Example: Parameter Name1, Default Value1, Effective Value1, Parameter Name2, Default Value2, Effective Value2
        If ($c -ge 11) {
          $Position = [System.Math]::Ceiling(($c / 3)-3)
          $tempColumn = $Column + "$Position"
        }
        If ([bool]($templateObject)) {
          $object.$tempColumn = $arrRow[$c]
        }
        Else { $object | Add-Member -MemberType NoteProperty -Name $tempColumn -Value "$($arrRow[$c])" }

        If ($Column -eq 'Rule/Monitor Name') {
          # If DisplayName (DN) does not already exist in set
          If (-not [bool]($DN = $DNHash.($arrRow[$c])) ) {
            # Find the DisplayName
            switch ($arrRow[8]) #Assuming this column header (no.9) is consistently "Type"
            {
              'Monitor' {
                $DN = (Get-SCOMMonitor -Name $arrRow[$c]).DisplayName
              }
              'Rule' {
                $DN = (Get-SCOMRule -Name $arrRow[$c]).DisplayName
              }
              Default {
                Write-Verbose "Problem in function: 'MAKEOBJECT'. Column header should be either 'Monitor' or 'Rule'." 
                Write-Verbose "This happens occassionally when a row in the export file is messed up."
                Write-Verbose "This is due to a glitch in the native OpsMan cmdlet: Export-SCOMEffectiveMonitoringConfiguration. Not much you can do about it." 
                Continue nextRow              
              }
            }

            # If no DN exists for the workflow, set a default
            If (-Not([bool]$DN)) {
              $DN = "N/A"
            }
            Else{
              $DNHash.Add($arrRow[$c],$DN)
            }
          }
          # DN Exists, add it to the hash table for fast lookup. Also add it to the catalog/array of known DNs so it can be saved and used again
          # next time for fast lookup.

          If ([bool]($templateObject)) {
            $object.'Rule/Monitor DisplayName' = $DN
          }
          Else { $object | Add-Member -MemberType NoteProperty -Name "Rule/Monitor DisplayName" -Value $DN }
        }
        $c++
      }
      $r++
      $mainArray += $object
      If (-not [bool]($templateObject)) {
        $templateObject = $object.PsObject.Copy()
      }
      Remove-Variable -name object,DN -ErrorAction SilentlyContinue
    }
    # Build a simple array to hold unique Name,DisplayName values so that it can be exported easily to a CSV file.
    # This cached csv file will significantly speed up the script next time it runs.
    ForEach ($Key in $DNHash.Keys){
      $tmpObj = New-Object -TypeName PSObject
      $tmpObj | Add-Member -MemberType NoteProperty -Name "Rule/Monitor Name" -Value $Key
      $tmpObj | Add-Member -MemberType NoteProperty -Name "Rule/Monitor DisplayName" -Value $DNHash.$Key
      $arrTmpDN += $tmpObj
    }
    $mainArray | Export-Csv -Path $strMergedFilePath -Force -Encoding UTF8 -Delimiter '|' -NoTypeInformation
    $arrTmpDN | Export-Csv -Path $savedDNs -Force -Encoding UTF8 -NoTypeInformation
    Return $mainArray
  }
  ###################################################################################################

  Function AddObjPath {
    Param (
      [string]$objPath,
      [string]$OutFilePath
    )

    $arrContent = @()
    $arrContent += 'Path'+'|'+(Get-Content -LiteralPath $OutFilePath | Select-Object -First 1)

    (Get-Content -LiteralPath $OutFilePath | Select-Object -Skip 1) | ForEach-Object {
      If ($objPath.Length -lt 5) {
        $arrContent += '|'+$_
      }
      Else {
        $arrContent += $objPath+'|'+$_
      }
    } 
    Set-Content -Path $OutFilePath -Value $arrContent -Force
  }
  
  # ---------------------------------------------------------------------------------------------------------------------------------------------------

  If (!(Test-Path $TargetFolder)) {
    Write-Verbose "TargetFolder [ $($TargetFolder) ] does not exist. Creating it now..."
    new-item -ItemType Directory -Path $TargetFolder
    If (!(Test-Path $TargetFolder)) {
      Write-Error "Unable to create TargetFolder: $TargetFolder. Exiting."
      Cleanup
      Exit
    }
    Else {
      Write-Verbose "Created TargetFolder successfully. "
    }
  }

  $elapsed_enumeration = [System.Diagnostics.Stopwatch]::StartNew()

  # If group name is provided...
  If ($SCOMGroupName) {
    $choice='group'
    $objects = @(Get-SCOMGroup -DisplayName $SCOMGroupName | Get-SCOMClassInstance)
    If (-not($objects)) {Write-Error "Unable to get group: [ $($SCOMGroupName) ]."
      Write-Verbose "To troubleshoot, run this command:`n`n Get-SCOMGroup -DisplayName '$SCOMGroupName' | Get-SCOMClassInstance `n"
      Write-Error "Exiting...";
      Cleanup
      Exit
    }
    Else {
      Write-Verbose "Success getting group: [ $($SCOMGroupName) ]."
    }

    $count = $objects.GetRelatedMonitoringObjects().Count
    "" # Cheap formatting
    ""
    Write-Host "This will output ALL monitoring configuration for group: " -ForegroundColor Cyan -BackgroundColor Black -NoNewline; `
    Write-Host "[" -ForegroundColor Red -BackgroundColor Black -NoNewline; `
    Write-Host "$($SCOMGroupName)" -ForegroundColor Yellow -BackgroundColor Black -NoNewline; `
    Write-Host "]" -ForegroundColor Red -BackgroundColor Black

    Write-Host "There are: " -ForegroundColor Green -BackgroundColor Black -NoNewline; `
    Write-Host "[" -ForegroundColor Red -BackgroundColor Black -NoNewline; `
    Write-Host "$($count)" -ForegroundColor Yellow -BackgroundColor Black -NoNewline; `
    Write-Host "]" -ForegroundColor Red -BackgroundColor Black -NoNewline; `
    Write-Host " nested objects in that group." -ForegroundColor Green -BackgroundColor Black
    Write-Host "This might take a little while depending on how large the group is and how many hosted objects exist!"  -ForegroundColor Green -BackgroundColor Black
    "" # Cheap formatting
  }

  # If ID is provided...
  ElseIf ($ID) {
    $choice='ID'
    Write-Verbose "Getting class instance with ID: [ $($ID) ] "
    $objects = (Get-SCOMClassInstance -Id $ID)
    If (-not($objects)) {
      Write-Error "Unable to get class instance for ID: [ $($ID) ] "
      Write-Verbose "To troubleshoot, use this command:`n`n Get-SCOMClassInstance -Id '$ID' `n"
      Write-Error "Exiting...";
      Cleanup
      Exit
    }
    Else {
      Write-Verbose "Success getting class instance with ID: [ $($ID) ], DisplayName: [ $($ID.DisplayName) ]."
    }

    $count = $objects.GetRelatedMonitoringObjects().Count
    "" # Cheap formatting
    ""
    Write-Host "This will output ALL monitoring configuration for object: " -ForegroundColor Cyan -BackgroundColor Black -NoNewline; `
    Write-Host "[" -ForegroundColor Red -BackgroundColor Black -NoNewline; `
    Write-Host "$($objects.DisplayName) , " -ForegroundColor Yellow -BackgroundColor Black -NoNewline; `
    Write-Host "ID: $ID " -ForegroundColor Gray -BackgroundColor Black -NoNewline; `
    Write-Host "]" -ForegroundColor Red -BackgroundColor Black
    Write-Host "There are: " -ForegroundColor Green -BackgroundColor Black -NoNewline; `
    Write-Host "[" -ForegroundColor Red -BackgroundColor Black -NoNewline; `
    Write-Host "$($count)" -ForegroundColor Yellow -BackgroundColor Black -NoNewline; `
    Write-Host "]" -ForegroundColor Red -BackgroundColor Black -NoNewline; `
    Write-Host " related monitoring objects."  -ForegroundColor Green -BackgroundColor Black
    Write-Host "This might take a little while depending on how many hosted objects exist !" -ForegroundColor Green -BackgroundColor Black
    "" # Cheap formatting
  }
  # Assume individul computer name is provided...
  ElseIf ($ComputerName){
    $choice='ComputerName'
    # $objects = @(Get-SCOMClass -Name "Microsoft.Windows.Computer" | Get-SCOMClassInstance | Where-Object {$ComputerName -contains $_.DisplayName } )
    # This approach should prove to be more efficient for environments with more than 40-ish Computers/agents.
    $ClassName = 'Microsoft.Windows.Computer'
    $ComputerClass = (Get-SCClass -Name $ClassName)
    If (-not($ComputerClass)) {
      Write-Error "Unable to get class: [ $ClassName ]."
      Write-Verbose "To troubleshoot, use this command:`n`n Get-SCOMClass -Name '$ClassName' `n"
      Write-Error "Exiting...";
      Cleanup
      Exit
    }
    Else {
      Write-Verbose "Success getting class object with name: [ $($ClassName) ]."
    }

    Write-Verbose "Getting class instance of [ $($ClassName) ] with DisplayName of [ $($ComputerName) ]..."
    $objects = @(Get-SCOMClassInstance -DisplayName $ComputerName | Where-Object {$_.LeastDerivedNonAbstractManagementPackClassId -like $ComputerClass.Id.Guid} )
    If (-not($objects)) {
      Write-Error "Unable to get class instance for DisplayName: [ $($ComputerName) ] "
      Write-Verbose "To troubleshoot, use this command:`n`n `$ComputerClass = (Get-SCOMClass -Name '$ClassName') "
      Write-Verbose " Get-SCOMClassInstance -DisplayName '$ComputerName' | Where-Object {`$_.LeastDerivedNonAbstractManagementPackClassId -like `$ComputerClass.Id.Guid} `n"
      Write-Error "Exiting...";
      Cleanup
      Exit
    }
    Else {
      Write-Verbose "Success getting class instance for DisplayName: [ $($ComputerName) ] "
    }
    
    $count = $objects.GetRelatedMonitoringObjects().Count
    "" # Cheap formatting
    ""
    Write-Host "This will output ALL monitoring configuration for Computer: " -ForegroundColor Cyan -BackgroundColor Black -NoNewline; `
    Write-Host "[" -ForegroundColor Red -BackgroundColor Black -NoNewline; `
    Write-Host "$($objects.DisplayName)" -ForegroundColor Yellow -BackgroundColor Black -NoNewline; `
    Write-Host "]" -ForegroundColor Red -BackgroundColor Black
    Write-Host "There are: " -ForegroundColor Green -BackgroundColor Black -NoNewline; `
    Write-Host "[" -ForegroundColor Red -BackgroundColor Black -NoNewline; `
    Write-Host "$($count)" -ForegroundColor Yellow -BackgroundColor Black -NoNewline; `
    Write-Host "]" -ForegroundColor Red -BackgroundColor Black -NoNewline; `
    Write-Host " related monitoring objects."  -ForegroundColor Green -BackgroundColor Black
    Write-Host "This might take a little while depending on how many hosted objects exist !"  -ForegroundColor Green -BackgroundColor Black
    "" # Cheap formatting
  }
  Else{
    #This should never happen because of parameter validation
    Write-Host "No input provided. Exiting..."
    Cleanup
    Exit
  }


  # If no OutputFileName exists, then simply use default.
  If (-not($OutputFileName)) {
    $OutputFileName = "Merged_"+".csv"
  }
  Else {
    $tempIndex = $OutputFileName.LastIndexOf('.')
    If ($tempIndex -gt 1) { $temp = $OutputFileName.Substring(0, $tempIndex) }
    Else { $temp = $OutputFileName }
    $OutputFileName = "Merged_"+$temp+".csv"
  }

  If (-not($NoConfirm)){
    # If output directory already contains file, this will notify the user. You may not want the merge operation to include other/older/foreign CSV files.
    $existingFiles = Get-ChildItem -LiteralPath $TargetFolder -File
    If ($existingFiles){
      Write-Host "CAUTION: Files already exist in the output directory [ $($TargetFolder) ]!"
      Write-Host "Delete existing files???." -ForegroundColor Red -BackgroundColor Yellow
    }
    # Force user to acknowledge prompt.
    While ($choice -notmatch '[y]|[n]'){
      $choice = Read-Host -Prompt  "Continue? (Y/N) `n"
    }
  } 
  Else {
    $choice = 'Y'
  }

  Switch ($choice)
  {
    "y" { 
      Write-Host "Proceed..." -BackgroundColor Black -ForegroundColor Cyan 
      Get-ChildItem -LiteralPath $TargetFolder -File -Filter *.csv | Remove-Item -Force -Verbose
    }
    "n" {Write-Host "Exiting..." -BackgroundColor Yellow -ForegroundColor Red; Exit; }
    Default {Write-Host "Must select 'Y' to proceed or 'N' to exit." -BackgroundColor Yellow -ForegroundColor Red; $choice ="" ;}
  }
    
  

  # Iterators used for nicely formatted output.

  $i=1
  # Iterate through the objects (including hosted instances) and dig out all related configs for rules/monitors.
  $objects | ForEach-Object `
  {
    $DN = (CleanName -uglyString $_.DisplayName)
    $objPath =  CleanName -uglyString $_.Path
    $OutFilePath = (Join-Path $TargetFolder "($objPath)_$($DN).csv" )
    Export-SCOMEffectiveMonitoringConfiguration -Instance $_ -Path $OutFilePath
    # Make sure header of each output file contains the object Path.
    AddObjPath -objPath $objPath -OutFilePath $OutFilePath
    
    Write-Host "$($i): " -ForegroundColor Cyan -NoNewline; `
    Write-Host "[" -ForegroundColor Red -NoNewline; `
    Write-Host "$($_.Path)" -ForegroundColor Yellow -NoNewline; `
    Write-Host "]" -ForegroundColor Red -NoNewline; `
    Write-Host " $($_.FullName)"  -ForegroundColor Green
    $r=1   #for progress bar calculation below

    $related = @($_.GetRelatedMonitoringObjects())
    Write-Verbose "There are $($related.Count) 'related' monitoring objects for $($_.DisplayName)."
    $related | ForEach-Object `
    -Process {
      $percent = [math]::Round((($r / $related.Count) *100),0)
      Write-Progress -Activity "** What's happening? **" -status "Getting your data. Be patient! [Percent: $($percent)]" -PercentComplete $percent
      $DN = (($($_.DisplayName).Replace(':','_')).Replace('/','_')).Replace('\','_')
      $objPath =  CleanName -uglyString $_.Path
      $OutFilePath = (Join-Path $TargetFolder "($objPath)_$($DN).csv" )
      Export-SCOMEffectiveMonitoringConfiguration -Instance $_ -Path $OutFilePath
      # Make sure header of each output file contains the object Path.
      AddObjPath -objPath $objPath -OutFilePath $OutFilePath
      
      Write-Host "$($i): " -ForegroundColor Cyan -NoNewline; `
      Write-Host "[" -ForegroundColor Red -NoNewline; `
      Write-Host "$($_.Path)" -ForegroundColor Yellow -NoNewline; `
      Write-Host "]" -ForegroundColor Red -NoNewline; `
      Write-Host " $($_.FullName)"  -ForegroundColor Green
      $i++   # formatting, total line numbers
      $r++   # this object's hosted items, for progress bar calculation above
    }
  }

  $Enumeration_TimeSeconds = "{0:N4}" -f $elapsed_enumeration.Elapsed.TotalSeconds
  $elapsed_merge = [System.Diagnostics.Stopwatch]::StartNew()

  # ------ Merge Operation ------
  MergeFiles -strPath $TargetFolder -strOutputFileName $OutputFileName
  # ------ Merge Operation ------

  $Merge_TimeSeconds = "{0:N4}" -f $elapsed_merge.Elapsed.TotalSeconds
  Write-Host "Enumeration Duration: `t" -ForegroundColor Green -BackgroundColor Black -NoNewline; `
  Write-Host "[" -ForegroundColor Red -BackgroundColor Black -NoNewline; `
  Write-Host "$($Enumeration_TimeSeconds)" -ForegroundColor Yellow -BackgroundColor Black -NoNewline; `
  Write-Host "]" -ForegroundColor Red -BackgroundColor Black -NoNewline; `
  Write-Host " seconds."  -ForegroundColor Green -BackgroundColor Black

  Write-Host "Merge Duration: `t" -ForegroundColor Green -BackgroundColor Black -NoNewline; `
  Write-Host "[" -ForegroundColor Red -BackgroundColor Black -NoNewline; `
  Write-Host "$($Merge_TimeSeconds)" -ForegroundColor Yellow -BackgroundColor Black -NoNewline; `
  Write-Host "]" -ForegroundColor Red -BackgroundColor Black -NoNewline; `
  Write-Host " seconds."  -ForegroundColor Green -BackgroundColor Black

  Write-Host "Formatting output for Grid View. This might take a minute..."  -ForegroundColor Cyan -BackgroundColor Black
  $elapsed_makeobject = [System.Diagnostics.Stopwatch]::StartNew()
  [string]$strMergedFilePath = (Join-Path $TargetFolder $OutputFileName)
  $objBlob = MakeObject -strMergedFilePath $strMergedFilePath
  $MakeObject_TimeSeconds = "{0:N4}" -f $elapsed_makeobject.Elapsed.TotalSeconds

  Write-Host "Grid View Format Duration: " -ForegroundColor Green -BackgroundColor Black -NoNewline; `
  Write-Host "[" -ForegroundColor Red -BackgroundColor Black -NoNewline; `
  Write-Host "$($MakeObject_TimeSeconds)" -ForegroundColor Yellow -BackgroundColor Black -NoNewline; `
  Write-Host "]" -ForegroundColor Red -BackgroundColor Black -NoNewline; `
  Write-Host " seconds."  -ForegroundColor Green -BackgroundColor Black

  If (-not($NoGridview)){
    $objBlob  | Out-Gridview -Title "Your Effective Configuration for $choice :"
  }

}#End Function
#######################################################################

<#
    .SYNOPSIS
    This script will get all rule and monitor knowledge article content and output the information to separate files (Rules.html and Monitors.html) in the output folder path specified.
 
    .PARAMETER OutFolder
    The output folder path where output files will be created. Must be a container/directory, not a file.
 
    .PARAMETER ManagementPack
    A collection/array of one or more management pack objects.
 
    .PARAMETER MgmtServerFQDN
    Alias: ManagementServer
    Fully Qualified Domain Name of the SCOM management server.
 
    .PARAMETER NoKnowledgeExclude
    By default ALL workflows will be included in the output files. Enable this switch to exclude workflows which have no Knowledge Article content.
 
    .PARAMETER ExportCSV
    Export results to CSV files instead of default format (.html)
 
    .PARAMETER Topic
    This will customize the name of the output files. Useful when dumping multiple sets to the same output folder.
 
    .PARAMETER ShowResult
    Will open the Windows file Explorer to show output files.
 
    .EXAMPLE
    (Get-SCOMManagementPack -Name *.AD.*) | Export-SCOMKnowledge -OutFolder 'C:\MyReports' -Topic "AD_" -ShowResult
    In the example above the variable will be assigned a collection of all management pack objects with ".AD." in the name.
    That subset/collection of management pack objects will be passed into the script. The script will output workflow details for all Rules and Monitors contained in ALL management packs within that set.
    The output file names will be: "AD_Rules.html" and "AD_Monitors.html". Finally the script will open Windows File Explorer to the location of the output directory.
 
    .Example
    PS C:\> Export-SCOMKnowledge -OutFolder 'C:\Export' -ManagementPack (Get-SCOMManagementPack -Name "*ad.*") -Filter '201[0-6]'
    The command above will output all rules/monitors from management packs which contain 'ad.' in the Name and which contain '201x' in the Name or DisplayName of the workflow where 'x' represents any single digit 0-6 .
 
    .EXAMPLE
    PS C:\> Export-SCOMKnowledge -OutFolder "C:\Temp" -ManagementPack (Get-SCOMManagementPack -Name *SQL*) -Topic "SQL_Packs_"
    The command above will output workflow details for all Rules and Monitors contained in ALL management packs with "SQL" in the management pack Name.
    The output file names will be: "SQL_Packs_Rules.html" and "SQL_Packs_Monitors.html"
 
    .EXAMPLE
    PS C:\> Export-SCOMKnowledge -OutFolder "C:\MyReports" -ManagementServer "ms01.contoso.com" -NoKnowledgeExclude
    In the example above, the command will connect to management server: "ms01.contoso.com", will output workflow details for all Rules and Monitors (only if they contain a Knowledge Article) to two separate files in the specified folder. The output file names will be: "Rules.html" and "Monitors.html"
 
    .Example
    PS C:\> Export-SCOMKnowledge -OutFolder 'C:\Export' -ManagementPack (Get-SCOMManagementPack -Name "*ad.*") -Filter '(?=200[0-8])((?!Monitoring).)*$'
    The command above will output all rules/monitors from management packs which contain 'ad' in the Name and which contain '200x' in the Name or DisplayName of the workflow where 'x' is numbers 1-8 but excluding workflows that contain 'monitoring'.
 
    .Example
    This is a way to use management pack files (.mp and .xml) from a directory path with specific topics.
    The below script example will use management pack files which names match the array of Topics.
 
    ##---- CONFIGURE THESE VARIABLES ----##
    # Location of management pack files
    $inDir = 'C:\Program Files (x86)\System Center Management Packs\'
    # Output directory
    $outDir = 'C:\'
    # Management pack file names which match these strings will be selected
    $topics = "DHCP,DNS,FileServices,GroupPolicy,DFS,FileReplication"
    ##---- CONFIGURE THESE VARIABLES ----##
 
    $FilePaths =@()
    $h = @{}
    # Will export data for each management pack file name matching the topic name.
    ForEach ($topic in $topics.Split(',')){
    Write-Host "Topic: $topic" -BackgroundColor Black -ForegroundColor Yellow
    $FilePaths = Get-ChildItem -Recurse -Include "*$topic*.mp*","*$topic*.xml*" -File -Path $inDir | Select fullname
    $FilePaths | ForEach {$h.(Split-Path $_.FullName -Leaf) = $_.FullName }
    If (!($h.Count)){ Continue;} #If no matching file found, skip to next topic
    $h.Keys
    ""
    $MPs = (Get-SCOMManagementPack -ManagementPackFile ($h.Values | % {$_} ) )
    Export-SCOMKnowledge -OutFolder $outDir -ManagementPack $MPs -Topic $Topic
    }
 
    .LINK
    https://blogs.msdn.microsoft.com/tysonpaul/
 
    .NOTES
    Version: 1.12
    Author: Tyson Paul
    Date: 2016/5/3
    Blog: https://blogs.msdn.microsoft.com/tysonpaul/2018/07/18/scomhelper-powershell-module-a-scom-admins-best-friend/
 
    History:
    2017/12/05: Added Topic parameter to customize output file names. Added ShowResult switch.
    2017/10/16: Added support for regex filtering on DisplayName or Name of monitors/rules.
    2016/05/03: Added option to export to CSV. Although embedded tables in the KnowledgeArticle don't convert very well.
    2016/04/26: Improved formatting of output files. Now includes embedded parameter tables and restored article href links.
    2016/04/26: Fixed script to accept ManagementPack pipeline input
 
#>

Function Export-SCOMKnowledge {
  [CmdletBinding(DefaultParameterSetName='Parameter Set 1',
      SupportsShouldProcess=$true,
      SupportsPaging = $true,
  PositionalBinding=$false)]

  Param(
    #1
    [Parameter(Mandatory=$true,
        ValueFromPipeline=$false,
        Position=0,
    ParameterSetName='Parameter Set 1')]
    [ValidateNotNull()]
    [ValidateNotNullOrEmpty()]
    [string]$OutFolder,

    #2
    [Parameter(Mandatory=$false,
        ValueFromPipeline=$true,
        ValueFromPipelineByPropertyName=$true,
        ValueFromRemainingArguments=$false,
        Position=1,
    ParameterSetName='Parameter Set 1')]
    [ValidateNotNull()]
    [ValidateNotNullOrEmpty()]
    [System.Object[]]$ManagementPack,

    #3
    [Parameter(Mandatory=$false,
        ValueFromPipeline=$false,
        Position=2,
    ParameterSetName='Parameter Set 1')]
    [ValidateNotNull()]
    [ValidateNotNullOrEmpty()]
    [Alias("ManagementServer")]
    [string]$MgmtServerFQDN,

    #4
    [Parameter(Mandatory=$false,
        Position=3,
    ParameterSetName='Parameter Set 1')]
    [Switch]$NoKnowledgeExclude,

    #5
    [Parameter(Mandatory=$false,
        ValueFromPipeline=$false,
        Position=4,
    ParameterSetName='Parameter Set 1')]
    [ValidateNotNull()]
    [ValidateNotNullOrEmpty()]
    [string]$OpsDBServer,

    #6
    [Parameter(Mandatory=$false,
        ValueFromPipeline=$false,
        Position=5,
    ParameterSetName='Parameter Set 1')]
    [ValidateNotNull()]
    [ValidateNotNullOrEmpty()]
    [string]$OpsDBName,

    #7
    [Parameter(Mandatory=$false,
        ValueFromPipeline=$false,
        Position=6,
    ParameterSetName='Parameter Set 1')]
    [ValidateNotNull()]
    [ValidateNotNullOrEmpty()]
    [string]$Topic,

    #8
    [Parameter(Mandatory=$false,
        Position=7,
    ParameterSetName='Parameter Set 1')]
    [string[]]$Filter,

    #9
    [Parameter(Mandatory=$false,
        Position=8,
    ParameterSetName='Parameter Set 1')]
    [switch]$ExportCSV,

    #10
    [Parameter(Mandatory=$false,
        Position=9,
    ParameterSetName='Parameter Set 1')]
    [switch]$ShowResult=$false
  )

  #### UNCOMMENT FOR TESTING ####
  #$OutFolder = "c:\Test"

  #region Functions
  Begin {
    #------------------------------------------------------------------------------------
    Function ProcessArticle {
      Param(
        $article
      )
      If ($article -ne "None")    #some rules don't have any knowledge articles
      {
        #Retrieve and format article content
        $MamlText = $null
        $HtmlText = $null

        If ($null -ne $article.MamlContent)
        {
          $MamlText = $article.MamlContent
          $articleContent = fnMamlToHtml($MamlText)
        }

        If ($null -ne $article.HtmlContent)
        {
          $HtmlText = $article.HtmlContent
          $articleContent = CleanHTML($HtmlText)
        }
      }

      If ($null -eq $articleContent)
      {
        $articleContent = "No resolutions were found for this alert."
      }

      Return $articleContent
    }

    #------------------------------------------------------------------------------------
    Function ProcessWorkflows {
      Param(
        $Workflows,
        [string]$WFType
      )
      $thisWFType = $WFType
      $myWFCollectionObj = @()
      [int]$row=1
      ForEach ($thisWF in $Workflows) {
        Write-Progress -Activity "Processing $WFType" -status "Getting Alert [$($row)]: $($thisWF.DisplayName) " -percentComplete ($row / $($Workflows.count) * 100)
        $ErrorActionPreference = 'SilentlyContinue'
        $article = $thisWF.GetKnowledgeArticle($cultureInfo)
        If ($? -eq $false){
          $error.Remove($Error[0])
          $article = "None"
          If ($NoKnowledgeExclude){ Continue; }
        }
        Else{
          $articleContent = ProcessArticle $article
        }

        If ($ExportCSV) {
          $WFName = $($thisWF.Name)
          $WFDisplayName = $($thisWF.DisplayName)
        }
        Else {
          $WFName = '<name>' + $($thisWF.Name) + '</name>'
          $WFDisplayName = '<displayname>' + $($thisWF.DisplayName) + '</displayname>'
        }
        $WFDescription = $thisWF.Description
        If ($WFDescription.Length -lt 1) {$WFDescription = "None"}
        #region Get_alert_name
        If ($WFType -like "Rule") {
          $thisWFType = "$($WFType): " + "$($thisWF.WriteActionCollection.Name)"
          # Proceed only if rule is an "alert" rule.
          If ($thisWF.WriteActionCollection.Name -Like "GenerateAlert"){
            $AlertMessageID = $($thisWF.WriteActionCollection.Configuration).Split('"',3)[1]
            $Query = @"
Select [LTValue]
FROM [OperationsManager].[dbo].[LocalizedText]
WHERE ElementName like '$($AlertMessageID)'
AND LTStringType = '1'
 
"@


            $AlertDisplayName = Invoke-CLSqlCmd -Query $Query -Server $OpsDBServer -Database $OpsDBName
            If ($ExportCSV) { $AlertName = $AlertDisplayName }
            Else { $AlertName = '<alertname>' + $AlertDisplayName + '</alertname>' }
          }
          Else {
            If ($ExportCSV) { $AlertName = 'N/A' }
            Else { $AlertName = '<noalertname>N/A</noalertname>' }
          }
        }
        Else {
          # Workflow is not a rule, therefore it is a monitor.
          $Query = @"
Select LocalizedText.LTValue
From [OperationsManager].[dbo].[LocalizedText]
INNER JOIN [OperationsManager].[dbo].[Monitor]
on LocalizedText.LTStringId=Monitor.AlertMessage
Where Monitor.MonitorID like '$($thisWF.ID)'
AND LocalizedText.LTStringType = '1'
 
"@

          $AlertDisplayName = Invoke-CLSqlCmd -Query $Query -Server $OpsDBServer -Database $OpsDBName
          # Not all monitors generate an alert.
          If ($AlertDisplayName -like "EMPTYRESULT") {
            If ($ExportCSV) { $AlertName = 'N/A' }
            Else { $AlertName = '<noalertname>N/A</noalertname>' }
          }
          Else {
            If ($ExportCSV) { $AlertName = $AlertDisplayName }
            Else { $AlertName = '<alertname>' + $AlertDisplayName + '</alertname>' }
          }
        } #endregion Get_alert_name

        $WFID = $thisWF.ID
        $WFMgmtPackID = $thisWF.GetManagementPack().Name
        # Build the custom object to represent the workflow, add properties/values.
        $myWFObj = New-Object -TypeName System.Management.Automation.PSObject
        $myWFObj | Add-Member -MemberType NoteProperty -Name "Row" -Value $Row
        $myWFObj | Add-Member -MemberType NoteProperty -Name "DisplayName" -Value $WFDisplayName
        $myWFObj | Add-Member -MemberType NoteProperty -Name "AlertName" -Value $AlertName
        $myWFObj | Add-Member -MemberType NoteProperty -Name "KnowledgeArticle" -Value $articleContent
        $myWFObj | Add-Member -MemberType NoteProperty -Name "Description" -Value $WFDescription
        $myWFObj | Add-Member -MemberType NoteProperty -Name "ManagementPackID" -Value $WFMgmtPackID
        $myWFObj | Add-Member -MemberType NoteProperty -Name "Name" -Value $WFName
        $myWFObj | Add-Member -MemberType NoteProperty -Name "WorkflowType" -Value $thisWFType
        $myWFObj | Add-Member -MemberType NoteProperty -Name "ID" -Value $WFID

        $myWFCollectionObj += $myWFObj
        $Row++
      }

      Return $myWFCollectionObj
    }

    #------------------------------------------------------------------------------------
    Function fnMamlToHTML{
      
      param
      (
        $MAMLText
      )
      $HTMLText = "";
      $HTMLText = $MAMLText -replace ('xmlns:maml="http://schemas.microsoft.com/maml/2004/10"');

      $HTMLText = $HTMLText -replace ("<maml:section>");
      $HTMLText = $HTMLText -replace ("<maml:section >");
      $HTMLText = $HTMLText -replace ("</maml:section>");
      $HTMLText = $HTMLText -replace ("<section >");

      #ui = underline. Not going to bother with this html conversion
      $HTMLText = $HTMLText -replace ("<maml:ui>", "");
      $HTMLText = $HTMLText -replace ("</maml:ui>", "");

      #Only convert the maml tables if not exporting to CSV
      IF ($ExportCSV) {
        $HTMLText = $HTMLText -replace ("<maml:para>", " ");
        $HTMLText = $HTMLText -replace ("<maml:para />", " ");
        $HTMLText = $HTMLText -replace ("</maml:para>", " ");
        $HTMLText = $HTMLText -replace ("<maml:title>", "");
        $HTMLText = $HTMLText -replace ("</maml:title>", "");
        $HTMLText = $HTMLText -replace ("<maml:list>", "");
        $HTMLText = $HTMLText -replace ("</maml:list>", "");
        $HTMLText = $HTMLText -replace ("<maml:listitem>", "");
        $HTMLText = $HTMLText -replace ("</maml:listitem>", "");
        $HTMLText = $HTMLText -replace ("<maml:table>", "");
        $HTMLText = $HTMLText -replace ("</maml:table>", "");
        $HTMLText = $HTMLText -replace ("<maml:row>", "");
        $HTMLText = $HTMLText -replace ("</maml:row>", "");
        $HTMLText = $HTMLText -replace ("<maml:entry>", "");
        $HTMLText = $HTMLText -replace ("</maml:entry>", "");
        $HTMLText = $HTMLText -replace ('<tr><td><p>Name</p></td><td><p>Description</p></td><td><p>Default Value</p></td></tr>', 'Name Description DefaultValue');
      }
      Else {
        $HTMLText = $HTMLText -replace ("maml:para", "p");
        $HTMLText = $HTMLText -replace ("<maml:table>", "<table>");
        $HTMLText = $HTMLText -replace ("</maml:table>", "</table>");
        $HTMLText = $HTMLText -replace ("<maml:row>", "<tr>");
        $HTMLText = $HTMLText -replace ("</maml:row>", "</tr>");
        $HTMLText = $HTMLText -replace ("<maml:entry>", "<td>");
        $HTMLText = $HTMLText -replace ("</maml:entry>", "</td>");
        $HTMLText = $HTMLText -replace ("<maml:title>", "<h3>");
        $HTMLText = $HTMLText -replace ("</maml:title>", "</h3>");
        $HTMLText = $HTMLText -replace ("<maml:list>", "<ul>");
        $HTMLText = $HTMLText -replace ("</maml:list>", "</ul>");
        $HTMLText = $HTMLText -replace ("<maml:listitem>", "<li>");
        $HTMLText = $HTMLText -replace ("</maml:listitem>", "</li>");
        $HTMLText = $HTMLText -replace ('<tr><td><p>Name</p></td><td><p>Description</p></td><td><p>Default Value</p></td></tr>', '<th>Name</th><th>Description</th><th>Default Value</th>');
      }
      # Replace all maml links with html href formatted links
      while ($HTMLText -like "*<maml:navigationLink>*"){
        If ($HtmlText -like "*uri condition*" ) {
          $HTMLText = Fix-HREF -mystring $HTMLText -irregular
        }
        Else {
          $HTMLText = Fix-HREF -mystring $HTMLText
        }
      }
      Return $HTMLText;
    }
    #------------------------------------------------------------------------------------
    Function CleanHTML{
      
      param
      (
        $HTMLText
      )
      $TrimedText = "";
      $TrimedText = $HTMLText -replace ("&lt;", "<")
      $TrimedText = $TrimedText -replace ("&gt;", ">")
      $TrimedText = $TrimedText -replace ("&quot;", '"')
      $TrimedText = $TrimedText -replace ("&amp;", '&')
      $TrimedText;
    }

    #------------------------------------------------------------------------------------
    #Remove maml link formatting, replace with HTML
    Function Fix-HREF{
      Param(
        [string]$mystring,
        [switch]$irregular
      )
      If ($irregular){
        $href_link_tag_begin = '<maml:uri condition'
      }
      Else {
        $href_link_tag_begin = '<maml:uri href="'

        $href_name_tag_begin = '<maml:navigationLink><maml:linkText>'
        $href_name_tag_end = '</maml:linkText>'
        $href_link_tag_end = '" /></maml:navigationLink>'

        $href_name_length = ($mystring.IndexOf($href_name_tag_end)) - ( $mystring.IndexOf($href_name_tag_begin) + $href_name_tag_begin.Length)
        $href_name = $mystring.Substring( ($mystring.IndexOf($href_name_tag_begin) + $href_name_tag_begin.Length ), $href_name_length)

        $href_link_length = ($mystring.IndexOf($href_link_tag_end)) - ( $mystring.IndexOf($href_link_tag_begin) + $href_link_tag_begin.Length -1)
        $href_link = $mystring.Substring( ($mystring.IndexOf($href_link_tag_begin) + $href_link_tag_begin.Length ), $href_link_length -1)
      }
      $Chunk_Name = $mystring.Substring( $mystring.IndexOf($href_name_tag_begin), (($mystring.IndexOf($href_name_tag_end) + $href_name_tag_end.Length - 1) - $mystring.IndexOf($href_name_tag_begin) +1 ) )
      $Chunk_HREF = $mystring.Substring( $mystring.IndexOf($href_link_tag_begin), (($mystring.IndexOf($href_link_tag_end) + $href_link_tag_end.Length - 1) - $mystring.IndexOf($href_link_tag_begin) +1 ) )

      If ($irregular){
        $newstring = $mystring.Replace(("$Chunk_Name" + "$Chunk_HREF"), '' )
      }
      Else {
        #Example: <a href="http://www.bing.com">here</a> to go to Bing.
        $newstring = $mystring.Replace(("$Chunk_Name" + "$Chunk_HREF"), ('<a href="' + $href_link + '">' + "$href_name" + '</a>') )
      }
      Return $newstring
    }

    #------------------------------------------------------------------------------------
    Function Invoke-CLSqlCmd {

      param(
        [string]$Server,
        [string]$Database,
        [string]$Query,
        [int]$QueryTimeout = 30, #The time in seconds to wait for the command to execute. The default is 30 seconds.
        [int]$ConnectionTimeout = 15  #The time (in seconds) to wait for a connection to open. The default value is 15 seconds.
      )
      BEGIN {
      }
      PROCESS {

        try {
          $sqlConnection = New-Object System.Data.SqlClient.SqlConnection
          $sqlConnection.ConnectionString = "Server=$Server;Database=$Database;Trusted_Connection=True;Connection Timeout=$ConnectionTimeout;"
          $sqlConnection.Open()
          try {
            $sqlCmd = New-Object System.Data.SqlClient.SqlCommand
            $sqlCmd.CommandText = $Query
            $sqlCmd.CommandTimeout = $QueryTimeout
            $sqlCmd.Connection = $SqlConnection
            try {
              $Value = @()
              $sqlReader = $sqlCmd.ExecuteReader()
              #The default position of the SqlDataReader is before the first record.
              #Therefore, you must call Read to begin accessing any data.
              #Return Value: true if there are more rows; otherwise false.
              If ($sqlReader.Read()) { # $NUL #function returns true. pipe to nowhere.
                $Value = ($sqlReader.GetValue(0), $sqlReader.GetName(0))
                If ( ($Value[1].ToString().Length -eq 0) -and ($sqlReader.Fieldcount -eq 2) ) {
                  #Write-Host "NoName!" -foreground Yellow -background Red
                  try{
                    $ResultName = $sqlReader.GetValue(1)
                  } finally {
                  }
                  If ($ResultName) {
                    $Value[1]=$ResultName
                  }
                  Else{
                    $Value[1]='Name'
                  }
                }
                #Write-Host "Value2: $Value2"
              }
              Else{
                $Value = ("EMPTYRESULT",'Result')
              }
            }
            finally {
              $sqlReader.Close()
            }
          } finally {
            $sqlCmd.Dispose()
          }
        } finally {
          $sqlConnection.Close()
        }
      }
      END {
        Return $Value[0]
      }
    } #endregion Invoke-CLSqlCmd
    #------------------------------------------------------------------------------------


    #####################################################################################
    $ThisScript = $MyInvocation.MyCommand.Path
    $Rules=@()
    $Monitors=@()
    $InstallDirectory =  (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Microsoft Operations Manager\3.0\Setup" -Name "InstallDirectory").InstallDirectory
    $PoshModulePath = Join-Path (Split-Path $InstallDirectory) PowerShell
    $env:PSModulePath += (";" + "$PoshModulePath")

    # If Topic is provided, make sure it contains an underscore.
    If (($Topic.Length -ge 2) -and (($Topic.IndexOf('_')+1) -ne $Topic.Length) ) {
      $Topic = "$Topic"+"_"
    }

    #Get OpsDB Server name if not provided
    If (!($OpsDBServer)){
      $OpsDBServer = (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Microsoft Operations Manager\3.0\Setup" -Name "DatabaseServerName").DatabaseServerName
    }
    #Get OpsDB name if not provided
    If (!($OpsDBName)){
      $OpsDBName = (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Microsoft Operations Manager\3.0\Setup" -Name "DatabaseName").DatabaseName
    }
    # Add 2012 Functionality/cmdlets
    Import-Module OperationsManager

    If ($Error) {
      $modulepaths=$env:PSModulePath.split(';')
      $Error.Clear()
    }

    # If no mgmt server name has been set, assume that local host is the mgmt server. It's worth a shot.
    If ($MgmtServerFQDN -eq "") {
      #Get FQDN of local host executing the script (could be any mgmt server when using SCOM2012 Resource Pools)
      $objIPProperties = [System.Net.NetworkInformation.IPGlobalProperties]::GetIPGlobalProperties()
      If ($null -eq $objIPProperties.DomainName) {
        $MgmtServerFQDN = $objIPProperties.HostName
      }
      Else {
        $MgmtServerFQDN = $objIPProperties.HostName + "." +$objIPProperties.DomainName
      }
    }
    $Error.Clear()
    #Connect to Localhost Note: perhaps this can be done differently/cleaner/faster if connecting to self?
    Write-Host "Connecting to: $MgmtServerFQDN ..." -ForegroundColor Gray -BackgroundColor Black
    New-SCManagementGroupConnection -Computer $MgmtServerFQDN # | Out-Null
    If ($Error) {
      Write-Host "Failed to connect to: $MgmtServerFQDN ! Exiting. " -ForegroundColor Red -BackgroundColor Yellow
      Exit
    }
    Else {
      Write-Host "Connected to: $MgmtServerFQDN " -ForegroundColor Magenta -BackgroundColor Black
    }

    #Set Culture Info
    $cultureInfo = [System.Globalization.CultureInfo]'en-US'

    $cssHead = @"
<style>
displayname {
    color: blue;
    font: 16px Arial, sans-serif;
}
alertname {
    color: red;
    font: 16px Arial, sans-serif;
}
noalertname {
    color: grey;
    font: 16px Arial, sans-serif;
}
 
name {
    font: 10px Arial, sans-serif;
}
body {
    font: normal 14px Verdana, Arial, sans-serif;
}
 
table, th, td {
    border-collapse: collapse;
    border: 1px solid black;
}
 
th, td {
    padding: 10px;
    text-align: left;
}
tr:hover {background-color: #ccffcc}
 
th {
    background-color: #4CAF50;
    color: white;
}
 
 
</style>
 
"@


    # Make sure output folder exists
    If (-not (Test-Path -Path $OutFolder -PathType Container )) {
      New-Item -Path $OutFolder -ItemType Directory -Force -ErrorAction Stop
    }

    # If output files already exist, remove them
    "Rules.html","Monitors.html" | ForEach-Object {
      If (Test-Path -PathType Leaf (Join-Path $OutFolder $_) ) {
        Remove-Item -Path (Join-Path $OutFolder $_) -Force
      }
    }

  } #endregion Begin

  #region
  Process {

    # If a set of MPs is specified, only process workflows contained in the MPs.
    If ($ManagementPack){

      $ManagementPack | Select-Object DisplayName,Name,ID,Version | Format-Table -AutoSize

      # If filter keyword(s) exist, then filter the results according to the regex patterns submitted in the parameter value.
      If ($Filter) {
        Foreach ($ThisRegex in $Filter) {
          Write-Host "Getting all filtered Rules..." -ForegroundColor Yellow
          $Rules += (Get-SCOMRule -ManagementPack $ManagementPack | Where-Object {( $_.Name -match "$ThisRegex") -OR ( $_.DisplayName -match "$ThisRegex")} )
          Write-Host "Getting all filtered Monitors..." -ForegroundColor Yellow
          $Monitors += (Get-SCOMMonitor -ManagementPack $ManagementPack | Where-Object {( $_.Name -match "$ThisRegex") -OR ( $_.DisplayName -match "$ThisRegex")} )
        }
      }
      # If no filter(s) exist, then return all rules/mons from the designated MP.
      Else {
        Write-Host "Getting ALL Rules..." -ForegroundColor Yellow
        $Rules += (Get-SCOMRule -ManagementPack $ManagementPack )
        Write-Host "Getting ALL Monitors..." -ForegroundColor Yellow
        $Monitors += (Get-SCOMMonitor -ManagementPack $ManagementPack )
      }
    }
    # Else, get ALL workflows in ALL MPs
    Else {
      # If filter(s) exist, then filter the results according to the regex patterns submitted in the parameter value.
      If ($Filter) {
        Foreach ($ThisRegex in $Filter) {
          Write-Host "Getting all filtered Rules..." -ForegroundColor Yellow
          $Rules += (Get-SCOMRule | Where-Object {( $_.Name -match "$ThisRegex") -OR ( $_.DisplayName -match "$ThisRegex")} )
          Write-Host "Getting all filtered Monitors..." -ForegroundColor Yellow
          $Monitors += (Get-SCOMMonitor | Where-Object {( $_.Name -match "$ThisRegex") -OR ( $_.DisplayName -match "$ThisRegex")} )
        }
      }
      Else{
        Write-Host "Getting ALL Rules..." -ForegroundColor Yellow
        $Rules += (Get-SCOMRule)
        Write-Host "Getting ALL Monitors..." -ForegroundColor Yellow
        $Monitors += (Get-SCOMMonitor)

      }
    }

  } #endregion

  #region
  End {

    Write-Host "`nTotal Rules Found: $($Rules.Count)" -BackgroundColor Black -ForegroundColor Green
    $myRulesObj = ProcessWorkflows -Workflows $Rules -WFType "Rule"
    If (($ExportCSV)) {
      $myRulesObjTemp = $myRulesObj | ConvertTo-Csv
      $myRulesObj = CleanHTML $myRulesObjTemp | ConvertFrom-CSV
      Write-Host "Exporting rules to CSV: "$(Join-Path $OutFolder ($Topic +"Rules.csv")) -F Cyan
      #Export to CSV
      $myRulesObj | Export-Csv -Path $(Join-Path $OutFolder ($Topic +"Rules.csv")) -NoTypeInformation
    }
    Else {
      $RulesTempContent = $myRulesObj | ConvertTo-HTML -Title "SCOM Rules" -Head $cssHead
      $RulesTempContent =  CleanHTML $RulesTempContent
      $RulesTempContent = $RulesTempContent -Replace '<table>', '<table border="1" cellpadding="20">'
      $RulesTempContent >> (Join-Path $OutFolder ($Topic +"Rules.html") )
    }
    Write-Host "Total Monitors Found: $($Monitors.Count)" -BackgroundColor Black -ForegroundColor Green
    $myMonitorObj = ProcessWorkflows -Workflows $Monitors -WFType "Monitor"
    If (($ExportCSV)) {
      $myMonitorObjTemp = $myMonitorObj | ConvertTo-CSV
      $myMonitorObj = CleanHTML $myMonitorObjTemp | ConvertFrom-CSV
      Write-Host "Exporting monitors to CSV: "$(Join-Path $OutFolder ($Topic + "Monitors.csv")) -F Cyan
      #Export to CSV
      $myMonitorObj | Export-Csv -Path $(Join-Path $OutFolder ($Topic + "Monitors.csv")) -NoTypeInformation
    }
    Else {
      $MonitorTempContent = $myMonitorObj | ConvertTo-HTML  -Title "SCOM Monitors" -Head $cssHead
      $MonitorTempContent =  CleanHTML $MonitorTempContent
      $MonitorTempContent = $MonitorTempContent -Replace '<table>', '<table border="1" cellpadding="20">'
      $MonitorTempContent >> (Join-Path $OutFolder ($Topic + "Monitors.html") )
    }
    Write-host "Output folder: $OutFolder" -BackgroundColor Black -ForegroundColor Yellow

    If ($ShowResult) {
      Explorer.exe $OutFolder
    }
  } #endregion
}#End Function
#######################################################################

<#
    .SYNOPSIS
    This script will get workflow details for one or more alerts, including the Knowledge Article content (neatly formatted in HTML).
 
    .DESCRIPTION
    Accepts one parameter, the alert object array of one or more SCOM alert objects: Microsoft.EnterpriseManagement.Monitoring.MonitoringAlert[]
 
    .PARAMETER Alert
    The alert object(s) to be queried for the workflow details including the Knowledge article (if one exists).
 
    .PARAMETER MgmtServerFQDN
    Fully Qualified Domain Name of the SCOM management server.
    Alias: ManagementServer
 
    .PARAMETER OutputArticleOnly
    Will output only the Knowledge Article(s) content (neatly formatted in HTML). This may abe useful for situations where a service/ticketing connector is used to get alert properties.
 
    .EXAMPLE
    Get-SCOMAlert | Select-Object -First 1 | Get-SCOMAlertKnowledge
    Will display alert info (including Knowledge Article in HTML format) for the first alert object returned.
 
    .EXAMPLE
    PS C:\> Get-SCOMAlertKnowledge -Alert (Get-SCOMAlert | Select-Object -First 3) -OutputArticleOnly
    Will output only the HTML Knowledge Articles for the first 3 alert objects returned.
 
    .EXAMPLE
    PS C:\> Get-SCOMAlertKnowledge -Alert (Get-SCOMAlert -Name *sql*)
    Will output alert info (including Knowledge Article in HTML format) for any alerts with "sql" in the alert name.
 
    .NOTES
    Author: Tyson Paul
    Date: 2016/3/31
    Blog: https://blogs.msdn.microsoft.com/tysonpaul/2018/07/18/scomhelper-powershell-module-a-scom-admins-best-friend/
#>

Function Get-SCOMAlertKnowledge {
  [CmdletBinding(DefaultParameterSetName='Parameter Set 1',
      SupportsShouldProcess=$true,
      SupportsPaging = $true,
  PositionalBinding=$true)]
  Param(

    [Parameter(Mandatory=$true,
        ValueFromPipeline=$true,
        ValueFromPipelineByPropertyName=$false,
        ValueFromRemainingArguments=$false,
        Position=0,
    ParameterSetName='Parameter Set 1')]
    [ValidateNotNull()]
    [ValidateNotNullOrEmpty()]
    [Microsoft.EnterpriseManagement.Monitoring.MonitoringAlert[]]$Alert,

    [Parameter(Mandatory=$false,
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false,
        ValueFromRemainingArguments=$false,
        Position=1,
    ParameterSetName='Parameter Set 1')]
    [Alias("ManagementServer")]
    [string]$MgmtServerFQDN = "",

    [Parameter(Mandatory=$false,
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false,
        ValueFromRemainingArguments=$false,
        Position=2,
    ParameterSetName='Parameter Set 1')]
    [Switch]$OutputArticleOnly = $false

  )


  Begin {

    #------------------------------------------------------------------------------------
    Function ProcessArticle {
      Param(
        $article
      )
      If ($article -ne "None")    #some rules don't have any knowledge articles
      {
        #Retrieve and format article content
        $MamlText = $null
        $HtmlText = $null

        If ($null -ne $article.MamlContent)
        {
          $MamlText = $article.MamlContent
          $articleContent = fnMamlToHtml($MamlText)

        }

        If ($null -ne $article.HtmlContent)
        {
          $HtmlText = $article.HtmlContent
          $articleContent = fnTrimHTML($HtmlText)
        }
      }

      If ($null -eq $articleContent)
      {
        $articleContent = "No resolutions were found for this alert."
      }

      Return $articleContent
    }

    #------------------------------------------------------------------------------------
    Function ProcessWorkflow {
      Param(
        $Workflows,
        [string]$WFType
      )
      $myWFCollectionObj = @()

      ForEach ($thisWF in $Workflows) {
        $ErrorActionPreference = 'SilentlyContinue'
        $article = $thisWF.GetKnowledgeArticle($cultureInfo)
        If ($? -eq $false){
          $error.Remove($Error[0])
          $article = "None"
        }
        Else{
          $articleContent = ProcessArticle $article
        }

        $WFName = $thisWF.Name
        $WFDisplayName = $thisWF.DisplayName
        $WFDescription = $thisWF.Description
        $WFID = $thisWF.ID
        If ($WFDescription.Length -lt 1) {$WFDescription = "None"}

        #Note: Only a small subset of alert properties are gathered here. Add additional Note Properties as needed using the format below.
        $myWFObj = New-Object -TypeName System.Management.Automation.PSObject
        $myWFObj | Add-Member -MemberType NoteProperty -Name "WorkflowType" -Value $WFType
        $myWFObj | Add-Member -MemberType NoteProperty -Name "Name" -Value $WFName
        $myWFObj | Add-Member -MemberType NoteProperty -Name "DisplayName" -Value $WFDisplayName
        $myWFObj | Add-Member -MemberType NoteProperty -Name "Description" -Value $WFDescription
        $myWFObj | Add-Member -MemberType NoteProperty -Name "ID" -Value $WFID
        $myWFObj | Add-Member -MemberType NoteProperty -Name "KnowledgeArticle" -Value $articleContent
        $myWFCollectionObj += $myWFObj
      }

      Return $myWFCollectionObj
    }

    #------------------------------------------------------------------------------------
    Function fnMamlToHTML{
      
      param
      (
        $MAMLText
      )
      $HTMLText = "";
      $HTMLText = $MAMLText -replace ('xmlns:maml="http://schemas.microsoft.com/maml/2004/10"');
      $HTMLText = $HTMLText -replace ("maml:para", "p");
      $HTMLText = $HTMLText -replace ("maml:");
      $HTMLText = $HTMLText -replace ("</section>");
      $HTMLText = $HTMLText -replace ("<section>");
      $HTMLText = $HTMLText -replace ("<section >");
      $HTMLText = $HTMLText -replace ("<title>", "<h3>");
      $HTMLText = $HTMLText -replace ("</title>", "</h3>");
      $HTMLText = $HTMLText -replace ("<listitem>", "<li>");
      $HTMLText = $HTMLText -replace ("</listitem>", "</li>");
      $HTMLText;
    }
    #------------------------------------------------------------------------------------
    Function fnTrimHTML($HTMLText){
      $TrimedText = "";
      $TrimedText = $HTMLText -replace ("&lt;", "<")
      $TrimedText = $TrimedText -replace ("&gt;", ">")
      <# $TrimedText = $TrimedText -replace ("<html>")
          $TrimedText = $TrimedText -replace ("<HTML>")
          $TrimedText = $TrimedText -replace ("</html>")
          $TrimedText = $TrimedText -replace ("</HTML>")
          $TrimedText = $TrimedText -replace ("<body>")
          $TrimedText = $TrimedText -replace ("<BODY>")
          $TrimedText = $TrimedText -replace ("</body>")
          $TrimedText = $TrimedText -replace ("</BODY>")
          $TrimedText = $TrimedText -replace ("<h1>", "<h3>")
          $TrimedText = $TrimedText -replace ("</h1>", "</h3>")
          $TrimedText = $TrimedText -replace ("<h2>", "<h3>")
          $TrimedText = $TrimedText -replace ("</h2>", "</h3>")
          $TrimedText = $TrimedText -replace ("<H1>", "<h3>")
          $TrimedText = $TrimedText -replace ("</H1>", "</h3>")
          $TrimedText = $TrimedText -replace ("<H2>", "<h3>")
          $TrimedText = $TrimedText -replace ("</H2>", "</h3>")
      #>

      $TrimedText;
    }
    #------------------------------------------------------------------------------------

    ###########################################################################################


    $ThisScript = $MyInvocation.MyCommand.Path
    $InstallDirectory =  (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Microsoft Operations Manager\3.0\Setup" -Name "InstallDirectory").InstallDirectory
    $PoshModulePath = Join-Path (Split-Path $InstallDirectory) PowerShell
    $env:PSModulePath += (";" + "$PoshModulePath")

    # Add 2012 Functionality/cmdlets
    Import-Module OperationsManager

    If ($Error) {
      $modulepaths=$env:PSModulePath.split(';')
      # LogIt -EventID 9995 -Type $warn -Force -Message "Import-Module error. Env:psmodulepath: `n$($modulepaths)" ;
      $Error.Clear()
    }

    # If no mgmt server name has been set, assume that local host is the mgmt server. It's worth a shot.
    If ($MgmtServerFQDN -eq "") {
      #Get FQDN of local host executing the script (could be any mgmt server when using SCOM2012 Resource Pools)
      $objIPProperties = [System.Net.NetworkInformation.IPGlobalProperties]::GetIPGlobalProperties()
      If ($null -eq $objIPProperties.DomainName) {
        $MgmtServerFQDN = $objIPProperties.HostName
      }
      Else {
        $MgmtServerFQDN = $objIPProperties.HostName + "." +$objIPProperties.DomainName
      }
    }

    #Connect to Localhost Note: perhaps this can be done differently/cleaner/faster if connecting to self?
    New-SCManagementGroupConnection -Computer $MgmtServerFQDN | Out-Null
    # If ($Error) { LogIt -EventID 9995 -Type $warn -Message "Log any Errors..." ; $Error.Clear() }

    #Set Culture Info
    $cultureInfo = [System.Globalization.CultureInfo]'en-US'
    $objWFCollection = @()
  }

  Process{
    #Depending on how the alert object(s) are passed in, the ForEach may be needed. (parameter vs. piped)
    ForEach ($objAlert in $Alert)
    {
      $workflowID = $objAlert.MonitoringRuleId
      $bIsMonitorAlert = $objAlert.IsMonitorAlert
      If ($bIsMonitorAlert -eq $false) {
        $WFType = "Rule"
        $workflow = Get-SCOMRule -Id $workflowID
      }
      ElseIf ($bIsMonitorAlert -eq $true) {
        $WFType = "Monitor"
        $workflow = Get-SCOMMonitor -Id $workflowID
      }

      # The funciton being called is designed to accept one or more workflows.
      # It will return one or more custom objects with workfow details, including (most importantly) the KnowlegeArticle.
      $objWFCollection += ProcessWorkflow -Workflows $Workflow -WFType $WFType
    }
  } #End Process

  End{
    If ($OutputArticleOnly){
      Return $objWFCollection.KnowledgeArticle
    }
    Else{
      Return $objWFCollection
    }
  }
}#End Function
#######################################################################

<#
    .Synopsis
    Will clear SCOM agent cache on local machine
    .EXAMPLE
    PS C:\> Clear-SCOMCache -WaitSeconds 60 -HealthServiceStatePath 'D:\Program Files\Microsoft System Center 2012 R2\Operations Manager\Server\Health Service State'
    .EXAMPLE
    PS C:\> Clear-SCOMCache -WaitSeconds 15
    .INPUTS
    None
    .OUTPUTS
    None
    .NOTES
    Author: Tyson Paul
    Blog: https://blogs.msdn.microsoft.com/tysonpaul/2018/07/18/scomhelper-powershell-module-a-scom-admins-best-friend/
    Version: 1.2
    Date: 2012.10.09
 
    History: 2018.05.25
    Cleaned up. Revised a few things.
     
#>

Function Clear-SCOMCache {
 
  Param(
    [int]$WaitSeconds=30,
    [string]$HealthServiceStatePath = (Join-Path (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Microsoft Operations Manager\3.0\Setup" -Name "InstallDirectory").InstallDirectory 'Health Service State' )
  )

  $ServiceName = 'HealthService'
  Write-Host "Stopping service: $ServiceName" -f Cyan
  $StartTime = Get-Date
  Stop-Service $ServiceName -Verbose
  Do {
    $i++
    Write-Host "Waiting for $ServiceName to stop..." -f Cyan
    Start-Sleep -Milliseconds 3000
    If ($i -ge 8) {
      Write-Host "Failed to stop service: $ServiceName in the time allowed! " -b Yellow -f Red
      Start-Service $ServiceName | Out-Null
    }
  } while ( ((Get-Service $ServiceName).Status -ne "Stopped") -AND ((Get-Date) -le ($StartTime.AddSeconds($WaitSeconds) )) )

  Write-Host "Attempting to remove cache folder: $HealthServiceStatePath" -f Cyan
  Get-Item $HealthServiceStatePath | Remove-Item -Recurse -Force -Verbose
  Start-Sleep -Milliseconds 1000
  If (Test-Path -PathType Container $HealthServiceStatePath) {
    Write-Host "Failed to remove folder: $HealthServiceStatePath ! "  -b Yellow -f Red
  }
  Else {
    Write-Host "Success! Folder removed." -f Green
  }

  Write-Host "Starting service: $ServiceName" -f Cyan
  $StartTime = Get-Date
  Start-Service $ServiceName -Verbose
  Do {
    $i++
    Write-Host "Waiting for $ServiceName to start..." -f Cyan
    Start-Sleep -Milliseconds 3000
    If ($i -ge 8) {
      Write-Host "Failed to start service: $ServiceName in the time allowed! " -b Yellow -f Red
    }
  } while ( ((Get-Service $ServiceName).Status -ne "Running") -AND ((Get-Date) -le ($StartTime.AddSeconds($WaitSeconds) )) )

  If ( (Test-Path -PathType Container $HealthServiceStatePath) ){
    Write-Host "Cache folder auto-created: $HealthServiceStatePath ! " -f Green
  }
  Write-Host "SCOM agent Cache has been cleared. " -f Green

} #End Function
#######################################################################



<#
    .Synopsis
    Will remove obsolete aliases from unsealed management packs.
    .DESCRIPTION
    This will not alter the original file but rather will output modified versions to the designated output folder.
 
    .EXAMPLE
    PS C:\> Remove-SCOMObsoleteReferenceFromMPFile -inFile 'C:\Unsealed MPs\*.xml' -outDir 'C:\Usealed MPs\Modified MPs'
 
    .EXAMPLE
    PS C:\> Remove-SCOMObsoleteReferenceFromMPFile -inFile 'C:\Unsealed MPs\MyOverrides.xml' -outDir 'C:\Usealed MPs\Modified MPs'
    .EXAMPLE
    PS C:\> Remove-SCOMObsoleteReferenceFromMPFile -inFile (GetChildItem 'C:\Unsealed MPs\*.xml').FullName
 
    .EXAMPLE
    PS C:\> Get-ChildItem -Path 'C:\Unsealed MPs\*.xml' | Remove-SCOMObsoleteReferenceFromMPFile
 
    .NOTES
    Author: Tyson Paul
    Blog: https://blogs.msdn.microsoft.com/tysonpaul/2018/07/18/scomhelper-powershell-module-a-scom-admins-best-friend/
    Version: 1.0
    Date: 20018.05.09
#>

Function Remove-SCOMObsoleteReferenceFromMPFile {
  [CmdletBinding()]

  Param (
    # Path to input file(s) (YourUnsealedPack.xml)
    [Parameter(Mandatory=$true,
        ValueFromPipeline=$true,
        ValueFromPipelineByPropertyName=$true,
    Position=0)]
    [ValidateNotNull()]
    [ValidateNotNullOrEmpty()]
    [Alias("FullName")]
    [string[]]$inFile,

    # Path to output folder, to save modified .xml file(s)
    [Parameter(Mandatory=$false,
        ValueFromPipeline=$false,
    Position=1)]
    [string]$outDir
  )

  Begin{}
  Process{
    [System.Object[]]$paths = (Get-ChildItem -Path $inFile )
    ForEach ($FilePath in $paths){
      If (-not [bool]$outDir){
        $outDir = (Join-Path (Split-Path $FilePath -Parent) "MODIFIED")
      }
      If (-NOT (Test-Path -Path $outDir) ){
        New-Item -Path $outDir -ItemType Directory -Force -ErrorAction SilentlyContinue | Out-Null
      }
      Write-Host "Input File: " -NoNewline; Write-Host $FilePath -F Cyan
      Try{
        [xml]$xml = Get-Content -Path $FilePath
        $References = $xml.ManagementPack.Manifest.References
      }Catch{
        Continue
      }
      If (-NOT [bool]$References) {Continue}
      $References.SelectNodes("*") | ForEach-Object {
        # If neither the Alias or ID of the referenced MP is found within the body of the MP, remove the reference.
        If (-NOT (($xml.InnerXml -match ("$($_.Alias)!")) -or ($xml.InnerXml -match ("$($_.ID)!"))) ) {
          Write-Host "Obsolete Reference Removed: $($_.Alias) : $($_.ID)" -F Green
          $References.RemoveChild($_) | Out-Null
        }
      }
      $outFile = (Join-Path $outDir (Split-Path $FilePath -Leaf))
      Write-Host "Saving to file: " -NoNewline; Write-Host $outFile -F Yellow
      $Xml.Save($outFile)
    }
  }
  End{}
}# END FUNCTION
#######################################################################

<#
    .Synopsis
 
    .DESCRIPTION
    Tim Culham of www.culham.net wrote an awesome lightweight Daily Health Check Script
    which can be found here:
    http://www.culham.net/powershell/scom-2012-scom-2012-r2-daily-check-powershell-script-html-report/
 
    I have been meaning to write something similiar for awhile so decided to take his wonderfully written
    script/structure and extend it by adding in a number of the more in depth Database Health Checks/KH Useful
    SQL Queries that I frequently find myself asking customers to run when they are experiencing performance
    issues with the Ops & DW DB's.
 
    I will cleanup my code in a later version and add additional functionality but for now Kudo's to Tim!
 
    MBullwin 11/3/2014
    www.OpsConfig.com
 
    [ https://gallery.technet.microsoft.com/SCOM-Health-Check-fd2272ec ]
    As with all scripts I post, this is provided AS-IS without warrenty so please test first and use at your own risk.
 
    What this version of the script will give you:(Some of these are just features which are carried over from the original, many are added)
 
    01. Version/Service Pack/Edition of SQL for each SCOM DB Server
 
    02. Disk Space Info for Ops DB, DW DB, and associated Temp DB's
 
    03. Database Backup Status for all DB's except Temp.
 
    04. Top 25 Largest Tables for Ops DB and DW DB
 
    05. Number of Events Generated Per Day (Ops DB)
 
    06. Top 10 Event Generating Computers (Ops DB)
 
    07. Top 25 Events by Publisher (Ops DB)
 
    08. Number of Perf Insertions Per Day (Ops DB)
 
    09. Top 25 Perf Insertions by Object/Counter Name (Ops DB)
 
    10. Top 25 Alerts by Alert Count
 
    11. Alerts with a Repeat Count higher than 200
 
    12. Stale State Change Data
 
    13. Top 25 Monitors Changing State in the last 7 Days
 
    14. Top 25 Monitors Changing State By Object
 
    15. Ops DB Grooming History
 
    16. Snapshot of DW Staging Tables
 
    17. DW Grooming Retention
 
    18. Management Server checks (Works well on prem, seems to have some issues with gateways due to remote calls-if you see some errors flash by have no fear though I wouldn't necessarily trust the results coming back from a Gateway server in the report depending on firewall settings)
 
    19. Daily KPI
 
    20. MP's Modified in the Last 24 hours
 
    21. Overrides in Default MP Check
 
    22. Unintialized Agents
 
    23. Agent Stats (Healthy, Warning, Critical, Unintialized, Total)
 
    24. Agent Pending Management Summary
 
    25. Alert Summary
 
    26. Servers in Maintenance Mode
 
    For more details on this version checkout: www.OpsConfig.com
    .NOTES
    Author: MBullwin
    Blog: www.OpsConfig.com
    Version: v1.1
    Date: 11/3/2014
    Original Script: https://gallery.technet.microsoft.com/SCOM-Health-Check-fd2272ec
 
#>

Function Get-SCOMHealthCheckOpsConfig {
  ###################################################################################################################
  #
  # Tim Culham of www.culham.net wrote an awesome lightweight Daily Health Check Script
  # which can be found here:
  #
  # http://www.culham.net/powershell/scom-2012-scom-2012-r2-daily-check-powershell-script-html-report/
  #
  # I have been meaning to write something similiar for awhile so decided to take his wonderfully written
  # script/structure and extend it by adding in a number of the more in depth Database Health Checks/KH Useful
  # SQL Queries that I frequently find myself asking customers to run when they are experiencing performance
  # issues with the Ops & DW DB's.
  #
  # I will cleanup my code in a later version and add additional functionality but for now Kudo's to Tim!
  #
  #
  # MBullwin 11/3/2014
  # www.OpsConfig.com
  #
  # As with all scripts I post, this is provided AS-IS without warrenty so please test first and use at your own risk.
  ######################################################################################################################
  $StartTime=Get-Date

  # Check if the OperationsManager Module is loaded
  if(-not (Get-Module | Where-Object {$_.Name -eq "OperationsManager"}))
  {
    "The Operations Manager Module was not found...importing the Operations Manager Module"
    Import-module OperationsManager
  }
  else
  {
    "The Operations Manager Module is loaded"
  }

  # Connect to localhost when running on the management server or define a server to connect to.
  $connect = New-SCManagementGroupConnection -ComputerName localhost

  # The Name and Location of are we going to save this Report
  $CreateReportLocation = [System.IO.Directory]::CreateDirectory("c:\SCOM_Health_Check")
  $ReportLocation = "c:\SCOM_Health_Check"
  $ReportName = "$(get-date -format "yyyy-M-dd")-SCOM-HealthCheck.html"
  $ReportPath = "$ReportLocation\$ReportName"

  # Create header for HTML Report
  $Head = "<style>"
  $Head +="BODY{background-color:#CCCCCC;font-family:Calibri,sans-serif; font-size: small;}"
  $Head +="TABLE{border-width: 1px;border-style: solid;border-color: black;border-collapse: collapse; width: 98%;}"
  $Head +="TH{border-width: 1px;padding: 0px;border-style: solid;border-color: black;background-color:#293956;color:white;padding: 5px; font-weight: bold;text-align:left;}"
  $Head +="TD{border-width: 1px;padding: 0px;border-style: solid;border-color: black;background-color:#F0F0F0; padding: 2px;}"
  $Head +="</style>"


  # Retrieve the name of the Operational Database and Data WareHouse Servers from the registry.
  $reg = Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Microsoft Operations Manager\3.0\Setup\"
  $OperationsManagerDBServer = $reg.DatabaseServerName
  $OperationsManagerDWServer = $reg.DataWarehouseDBServerName
  # If the value is empty in this key, then we'll use the Get-SCOMDataWarehouseSetting cmdlet.
  If (!($OperationsManagerDWServer))
  {$OperationsManagerDWServer = Get-SCOMDataWarehouseSetting | Select-Object -expandProperty DataWarehouseServerName}

  $OperationsManagerDBServer = $OperationsManagerDBServer.ToUpper()
  $OperationsManagerDWServer = $OperationsManagerDWServer.ToUpper()

  $ReportingURL = Get-SCOMReportingSetting | Select-Object -ExpandProperty ReportingServerUrl
  $WebConsoleURL = Get-SCOMWebAddressSetting | Select-Object -ExpandProperty WebConsoleUrl
  <#
      # The number of days before Database Grooming
      # These are my settings, I use this to determine if someone has changed something
      # Feel free to comment this part out if you aren't interested
      $AlertDaysToKeep = 2
      $AvailabilityHistoryDaysToKeep = 2
      $EventDaysToKeep = 1
      $JobStatusDaysToKeep = 1
      $MaintenanceModeHistoryDaysToKeep = 2
      $MonitoringJobDaysToKeep = 2
      $PerformanceDataDaysToKeep = 2
      $StateChangeEventDaysToKeep = 2
 
      # SCOM Agent Heartbeat Settings
      $AgentHeartbeatInterval = 180
      $MissingHeartbeatThreshold = 3
  #>


  # SQL Server Function to query the Operational Database Server
  function Run-OpDBSQLQuery
  {
    Param($sqlquery)

    $SqlConnection = New-Object System.Data.SqlClient.SqlConnection
    $SqlConnection.ConnectionString = "Server=$OperationsManagerDBServer;Database=OperationsManager;Integrated Security=True"
    $SqlCmd = New-Object System.Data.SqlClient.SqlCommand
    $SqlCmd.CommandText = $sqlquery
    $SqlCmd.Connection = $SqlConnection
    $SqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter
    $SqlAdapter.SelectCommand = $SqlCmd
    $DataSet = New-Object System.Data.DataSet
    $SqlAdapter.Fill($DataSet) | Out-Null
    $SqlConnection.Close()
    $DataSet.Tables[0]
  }


  # SQL Server Function to query the Data Warehouse Database Server
  function Run-OpDWSQLQuery
  {
    Param($sqlquery)

    $SqlConnection = New-Object System.Data.SqlClient.SqlConnection
    $SqlConnection.ConnectionString = "Server=$OperationsManagerDWServer;Database=OperationsManagerDW;Integrated Security=True"
    $SqlCmd = New-Object System.Data.SqlClient.SqlCommand
    $SqlCmd.CommandText = $sqlquery
    $SqlCmd.Connection = $SqlConnection
    $SqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter
    $SqlAdapter.SelectCommand = $SqlCmd
    $DataSet = New-Object System.Data.DataSet
    $SqlAdapter.Fill($DataSet) | Out-Null
    $SqlConnection.Close()
    $DataSet.Tables[0]
  }


  # Retrieve the Data for the Majority of the Report
  # Truth is we probably don't need all of this data, but even on a busy environment it only takes a couple of mins to run.
  Write-Host "Retrieving Agents"
  $Agents = Get-SCOMAgent
  Write-Host "Retrieving Alerts"
  $Alerts = Get-SCOMAlert
  Write-Host "Retrieving Groups"
  $Groups = Get-SCOMGroup
  Write-Host "Retrieving Management Group"
  $ManagementGroup = Get-SCOMManagementGroup
  Write-Host "Retrieving Management Packs"
  $ManagementPacks = Get-SCManagementPack
  Write-Host "Retrieving Management Servers"
  $ManagementServers = Get-SCOMManagementServer
  Write-Host "Retrieving Monitors"
  $Monitors = Get-SCOMMonitor
  Write-Host "Retrieving Rules"
  $Rules = Get-SCOMRule

  # Check to see if the Reporting Server Site is OK
  $ReportingServerSite = New-Object System.Net.WebClient
  $ReportingServerSite = [net.WebRequest]::Create($ReportingURL)
  $ReportingServerSite.UseDefaultCredentials = $true
  $ReportingServerStatus = $ReportingServerSite.GetResponse() | Select-Object -expandProperty statusCode
  # This code can convert the "OK" Result to an Integer, like 200
  # (($web.GetResponse()).Statuscode) -as [int]

  # Check to see if the Web Server Site is OK
  $WebConsoleSite = New-Object System.Net.WebClient
  $WebConsoleSite = [net.WebRequest]::Create($WebConsoleURL)
  $WebConsoleSite.UseDefaultCredentials = $true
  $WebConsoleStatus = $WebConsoleSite.GetResponse() | Select-Object -expandProperty statusCode

  # SQL Server Function to query Size of the Database Server
  $DatabaseSize = @"
select a.FILEID,
[FILE_SIZE_MB]=convert(decimal(12,2),round(a.size/128.000,2)),
[SPACE_USED_MB]=convert(decimal(12,2),round(fileproperty(a.name,'SpaceUsed')/128.000,2)),
[FREE_SPACE_MB]=convert(decimal(12,2),round((a.size-fileproperty(a.name,'SpaceUsed'))/128.000,2)) ,
[GROWTH_MB]=convert(decimal(12,2),round(a.growth/128.000,2)),
NAME=left(a.NAME,15),
FILENAME=left(a.FILENAME,60)
from dbo.sysfiles a
"@


  #SQL Server Function to query Size of the TempDB
  $TempDBSize =@"
USE tempdb
select a.FILEID,
[FILE_SIZE_MB]=convert(decimal(12,2),round(a.size/128.000,2)),
[SPACE_USED_MB]=convert(decimal(12,2),round(fileproperty(a.name,'SpaceUsed')/128.000,2)),
[FREE_SPACE_MB]=convert(decimal(12,2),round((a.size-fileproperty(a.name,'SpaceUsed'))/128.000,2)) ,
[GROWTH_MB]=convert(decimal(12,2),round(a.growth/128.000,2)),
NAME=left(a.NAME,15),
FILENAME=left(a.FILENAME,60)
from dbo.sysfiles a
"@


  #SQL Server Function to query the version of SQL
  $SQLVersion =@"
SELECT SERVERPROPERTY('productversion') AS "Product Version", SERVERPROPERTY('productlevel') AS "Service Pack", SERVERPROPERTY ('edition') AS "Edition"
"@


  # Run the Size Query against the Operational Database and Data Warehouse Database Servers
  $OPDBSize = Run-OpDBSQLQuery $DatabaseSize
  $DWDBSize = Run-OpDWSQLQuery $DatabaseSize
  $OPTPSize = Run-OpDBSQLQuery $TempDBSize
  $DWTPSize = Run-OpDWSQLQuery $TempDBSize
  $OPSQLVER = Run-OpDBSQLQuery $SQLVersion
  $DWSQLVER = Run-OpDWSQLQuery $SQLVersion

  # Insert the Database Server details into the Report
  $ReportOutput += "<h2>Database Servers</h2>"
  $ReportOutput += "<p>Operational Database Server : $OperationsManagerDBServer</p>"
  $ReportOutput += $OPSQLVER | Select-Object "Product Version", "Service Pack", Edition | ConvertTo-Html -Fragment
  $ReportOutput += "<p>Data Warehouse Database Server : $OperationsManagerDWServer</p>"
  $ReportOutput += $DWSQLVER | Select-Object "Product Version", "Service Pack", Edition | ConvertTo-Html -Fragment

  # Insert the Size Results for the Operational Database into the Report
  $ReportOutput += "<h3>$OperationsManagerDBServer Operations Manager DB</h4>"
  $ReportOutput += $OPDBSize | Select-Object Name, FILE_SIZE_MB, SPACE_USED_MB, FREE_SPACE_MB, FILENAME | ConvertTo-HTML -fragment
  $ReportOutput += "<br></br>"
  $ReportOutput += "<h3>Operations Temp DB</h4>"
  $ReportOutput += $OPTPSize | Select-Object Name, FILE_SIZE_MB, SPACE_USED_MB, FREE_SPACE_MB, FILENAME | ConvertTo-HTML -fragment

  # Insert the Size Results for the Data Warehouse Database and TempDB into the Report
  $ReportOutput += "<br>"
  $ReportOutput += "<h3>$OperationsManagerDWServer Data Warehouse DB</h4>"
  $ReportOutput += $DWDBSize | Select-Object Name, FILE_SIZE_MB, SPACE_USED_MB, FREE_SPACE_MB, FILENAME | ConvertTo-HTML -fragment
  $ReportOutput += "<br></br>"
  $ReportOutput += "<h3>Data Warehouse Temp DB</h4>"
  $ReportOutput += $DWTPSize | Select-Object Name, FILE_SIZE_MB, SPACE_USED_MB, FREE_SPACE_MB, FILENAME | ConvertTo-HTML -fragment

  # SQL Query to find out how many State Changes there were yesterday
  $StateChangesYesterday = @"
-- How Many State Changes Yesterday?:
select count(*) from StateChangeEvent
where cast(TimeGenerated as date) = cast(getdate()-1 as date)
"@


  $StateChanges = Run-OpDBSQLQuery $StateChangesYesterday | Select-Object -ExpandProperty Column1 | Out-String

  $AllStats = @()

  $StatSummary = New-Object psobject
  $StatSummary | Add-Member noteproperty "Open Alerts" (($Alerts | Where-Object {$_.ResolutionState -ne 255}).count)
  $StatSummary | Add-Member noteproperty "Groups" ($Groups.Count)
  $StatSummary | Add-Member noteproperty "Monitors" ($Monitors.Count)
  $StatSummary | Add-Member noteproperty "Rules" ($Rules.Count)
  $StatSummary | Add-Member noteproperty "State Changes Yesterday" ($StateChanges | ForEach-Object {$_.TrimStart()} | ForEach-Object {$_.TrimEnd()})

  $AllStats += $StatSummary


  #SQL Query Top 10 Event generating computers
  $TopEventGeneratingComputers = @"
SELECT top 10 LoggingComputer, COUNT(*) AS TotalEvents
FROM EventallView
GROUP BY LoggingComputer
ORDER BY TotalEvents DESC
"@


  #SQL Query number of Events Generated per day
  $NumberOfEventsPerDay = @"
SELECT CASE WHEN(GROUPING(CONVERT(VARCHAR(20), TimeAdded, 101)) = 1)
THEN 'All Days'
ELSE CONVERT(VARCHAR(20), TimeAdded, 101) END AS DayAdded,
COUNT(*) AS NumEventsPerDay
FROM EventAllView
GROUP BY CONVERT(VARCHAR(20), TimeAdded, 101) WITH ROLLUP
ORDER BY DayAdded DESC
"@


  #SQL Query Most Common Events by Publishername
  $MostCommonEventsByPub = @"
SELECT top 25 Number AS "Event Number", Publishername, COUNT(*) AS TotalEvents
FROM EventAllView
GROUP BY Number, Publishername
ORDER BY TotalEvents DESC
"@


  #SQL Query the Number of Performance Insertions per Day
  $NumberofPerInsertsPerDay = @"
SELECT CASE WHEN(GROUPING(CONVERT(VARCHAR(20), TimeSampled, 101)) = 1)
THEN 'All Days' ELSE CONVERT(VARCHAR(20), TimeSampled, 101)
END AS DaySampled, COUNT(*) AS NumPerfPerDay
FROM PerformanceDataAllView
GROUP BY CONVERT(VARCHAR(20), TimeSampled, 101) WITH ROLLUP
ORDER BY DaySampled DESC
"@


  #SQL Query the Most common perf insertions by perf counter name
  $MostCommonPerfByN = @"
select top 25 pcv.objectname, pcv.countername, count (pcv.countername) as total from
performancedataallview as pdv, performancecounterview as pcv
where (pdv.performancesourceinternalid = pcv.performancesourceinternalid)
group by pcv.objectname, pcv.countername
order by count (pcv.countername) desc
"@


  #SQL Query the Top 25 Alerts by Alert Count
  $MostCommonAByAC = @"
SELECT Top 25 AlertStringName, Name, SUM(1) AS
AlertCount, SUM(RepeatCount+1) AS AlertCountWithRepeatCount
FROM Alertview WITH (NOLOCK)
GROUP BY AlertStringName, Name
ORDER BY AlertCount DESC
"@


  #SQL Query for Stale State Change Data
  $StaleStateChangeData = @"
declare @statedaystokeep INT
SELECT @statedaystokeep = DaysToKeep from PartitionAndGroomingSettings WHERE ObjectName = 'StateChangeEvent'
SELECT COUNT(*) as 'Total StateChanges',
count(CASE WHEN sce.TimeGenerated > dateadd(dd,-@statedaystokeep,getutcdate()) THEN sce.TimeGenerated ELSE NULL END) as 'within grooming retention',
count(CASE WHEN sce.TimeGenerated < dateadd(dd,-@statedaystokeep,getutcdate()) THEN sce.TimeGenerated ELSE NULL END) as '> grooming retention',
count(CASE WHEN sce.TimeGenerated < dateadd(dd,-30,getutcdate()) THEN sce.TimeGenerated ELSE NULL END) as '> 30 days',
count(CASE WHEN sce.TimeGenerated < dateadd(dd,-90,getutcdate()) THEN sce.TimeGenerated ELSE NULL END) as '> 90 days',
count(CASE WHEN sce.TimeGenerated < dateadd(dd,-365,getutcdate()) THEN sce.TimeGenerated ELSE NULL END) as '> 365 days'
from StateChangeEvent sce
"@


  #SQL Query Noisest monitors changing state in the past 7 days
  $NoisyMonitorData = @"
select distinct top 25
m.DisplayName as MonitorDisplayName,
m.Name as MonitorIdName,
mt.typename AS TargetClass,
count(sce.StateId) as NumStateChanges
from StateChangeEvent sce with (nolock)
join state s with (nolock) on sce.StateId = s.StateId
join monitorview m with (nolock) on s.MonitorId = m.Id
join managedtype mt with (nolock) on m.TargetMonitoringClassId = mt.ManagedTypeId
where m.IsUnitMonitor = 1
  -- Scoped to within last 7 days
AND sce.TimeGenerated > dateadd(dd,-7,getutcdate())
group by m.DisplayName, m.Name,mt.typename
order by NumStateChanges desc
"@


  #SQL Query Top 25 Monitors changing state by Object
  $NoisyMonitorByObject =@"
select distinct top 25
bme.DisplayName AS ObjectName,
bme.Path,
m.DisplayName as MonitorDisplayName,
m.Name as MonitorIdName,
mt.typename AS TargetClass,
count(sce.StateId) as NumStateChanges
from StateChangeEvent sce with (nolock)
join state s with (nolock) on sce.StateId = s.StateId
join BaseManagedEntity bme with (nolock) on s.BasemanagedEntityId = bme.BasemanagedEntityId
join MonitorView m with (nolock) on s.MonitorId = m.Id
join managedtype mt with (nolock) on m.TargetMonitoringClassId = mt.ManagedTypeId
where m.IsUnitMonitor = 1
   -- Scoped to specific Monitor (remove the "--" below):
   -- AND m.MonitorName like ('%HealthService%')
   -- Scoped to specific Computer (remove the "--" below):
   -- AND bme.Path like ('%sql%')
   -- Scoped to within last 7 days
AND sce.TimeGenerated > dateadd(dd,-7,getutcdate())
group by s.BasemanagedEntityId,bme.DisplayName,bme.Path,m.DisplayName,m.Name,mt.typename
order by NumStateChanges desc
"@


  #SQL Query Grooming Settings for the Operational Database
  $OpsDBGrooming =@"
SELECT
ObjectName,
GroomingSproc,
DaysToKeep,
GroomingRunTime,
DataGroomedMaxTime
FROM PartitionAndGroomingSettings WITH (NOLOCK)
"@



  #SQL Query DW DB Staging Tables
  $DWDBStagingTables = @"
select count(*) AS "Alert Staging Table" from Alert.AlertStage
"@


  $DWDBStagingTablesEvent =@"
select count (*) AS "Event Staging Table" from Event.eventstage
"@


  $DWDBStagingTablesPerf =@"
select count (*) AS "Perf Staging Table" from Perf.PerformanceStage
"@


  $DWDBStagingTablesState =@"
select count (*) AS "State Staging Table" from state.statestage
"@


  #SQL Query DW Grooming Retention
  $DWDBGroomingRetention =@"
select ds.datasetDefaultName AS 'Dataset Name', sda.AggregationTypeId AS 'Agg Type 0=raw, 20=Hourly, 30=Daily', sda.MaxDataAgeDays AS 'Retention Time in Days'
from dataset ds, StandardDatasetAggregation sda
WHERE ds.datasetid = sda.datasetid ORDER by "Retention Time in Days" desc
"@


  #SQL function to Query the Top 25 largest tables in a database
  $DWDBLargestTables =@"
SELECT TOP 25
a2.name AS [tablename], (a1.reserved + ISNULL(a4.reserved,0))* 8 AS reserved,
a1.rows as row_count, a1.data * 8 AS data,
(CASE WHEN (a1.used + ISNULL(a4.used,0)) > a1.data THEN (a1.used + ISNULL(a4.used,0)) - a1.data ELSE 0 END) * 8 AS index_size,
(CASE WHEN (a1.reserved + ISNULL(a4.reserved,0)) > a1.used THEN (a1.reserved + ISNULL(a4.reserved,0)) - a1.used ELSE 0 END) * 8 AS unused,
(row_number() over(order by (a1.reserved + ISNULL(a4.reserved,0)) desc))%2 as l1,
a3.name AS [schemaname]
FROM (SELECT ps.object_id, SUM (CASE WHEN (ps.index_id < 2) THEN row_count ELSE 0 END) AS [rows],
SUM (ps.reserved_page_count) AS reserved,
SUM (CASE WHEN (ps.index_id < 2) THEN (ps.in_row_data_page_count + ps.lob_used_page_count + ps.row_overflow_used_page_count)
ELSE (ps.lob_used_page_count + ps.row_overflow_used_page_count) END ) AS data,
SUM (ps.used_page_count) AS used
FROM sys.dm_db_partition_stats ps
GROUP BY ps.object_id) AS a1
LEFT OUTER JOIN (SELECT it.parent_id,
SUM(ps.reserved_page_count) AS reserved,
SUM(ps.used_page_count) AS used
FROM sys.dm_db_partition_stats ps
INNER JOIN sys.internal_tables it ON (it.object_id = ps.object_id)
WHERE it.internal_type IN (202,204)
GROUP BY it.parent_id) AS a4 ON (a4.parent_id = a1.object_id)
INNER JOIN sys.all_objects a2 ON ( a1.object_id = a2.object_id )
INNER JOIN sys.schemas a3 ON (a2.schema_id = a3.schema_id)
WHERE a2.type <> N'S' and a2.type <> N'IT'
"@


  #SQL Function to query and check backup status of SQL Databases
  $SQLBackupStatus =@"
SELECT
d.name,
DATEDIFF(Day, COALESCE(MAX(b.backup_finish_date), d.create_date), GETDATE()) AS [DaysSinceBackup]
FROM
sys.databases d
LEFT OUTER JOIN msdb.dbo.backupset b
ON d.name = b.database_name
WHERE
d.is_in_standby = 0
AND source_database_id is null
AND d.name NOT LIKE 'tempdb'
AND (b.[type] IN ('D', 'I') OR b.[type] IS NULL)
GROUP BY
d.name, d.create_date
"@


  # Run additional SQL Queries against the Operational Database
  $OPTOPALERT = Run-OpDBSQLQuery $TopEventGeneratingComputers
  $OPNUMEPERDAY = Run-OpDBSQLQuery $NumberOfEventsPerDay
  $OPMOSTCOMEVENT = Run-OpDBSQLQuery $MostCommonEventsByPub
  $OPPERFIPERD = Run-OpDBSQLQuery $NumberofPerInsertsPerDay
  $OPPERFIBYN = Run-OpDBSQLQuery $MostCommonPerfByN
  $OPTOPALERTC = Run-OpDBSQLQuery $MostCommonAByAC
  $OPSTALESTD = Run-OpDBSQLQuery $StaleStateChangeData
  $OPNOISYMON = Run-OpDBSQLQuery $NoisyMonitorData
  $OPNOISYMONOBJ = Run-OpDBSQLQuery $NoisyMonitorByObject
  $OPDBGROOM = Run-OpDBSQLQuery $OpsDBGrooming
  $OPLARGTAB = Run-OpDBSQLQuery $DWDBLargestTables
  $OPDBBACKUP = Run-OpDBSQLQuery $SQLBackupStatus

  #Run additional SQL Queries against the DW DB
  $DWDBSGTB = Run-OpDWSQLQuery $DWDBStagingTables
  $DWDBSGTBEV = Run-OpDWSQLQuery $DWDBStagingTablesEvent
  $DWDBSGTBPE = Run-OpDWSQLQuery $DWDBStagingTablesPerf
  $DWDBSGTBST = Run-OpDWSQLQuery $DWDBStagingTablesState
  $DWDBGRET = Run-OpDWSQLQuery $DWDBGroomingRetention
  $DWDBLARGETAB = Run-OpDWSQLQuery $DWDBLargestTables
  $DWDBBACKUP = Run-OpDWSQLQuery $SQLBackupStatus

  #Output to HTML Report

  $ReportOutput += "<h2>Operational Database Health</h2>"
  $ReportOutput += "<h3>Operations Database Backup Status</h3>"
  $ReportOutput += $OPDBBACKUP | Select-Object name, DaysSinceBackup | ConvertTo-HTML -Fragment
  $ReportOutput += "<h3>Operations Database Top 25 Largest Tables</h3>"
  $ReportOutput += $OPLARGTAB | Select-Object tablename, reserved, row_count, data, index_size, unused |ConvertTo-Html -Fragment
  $ReportOutput += "<h3>Number of Events Generated Per Day</h3>"
  $ReportOutput += $OPNUMEPERDAY | Select-Object NumEventsPerDay, DayAdded | ConvertTo-HTML -Fragment
  $ReportOutput += "<h3>Top 10 Event Generating Computers</h3>"
  $ReportOutput += $OPTOPALERT | Select-Object LoggingComputer, TotalEvents | ConvertTo-HTML -Fragment
  $ReportOutput += "<h3>Top 25 Events By Publisher</h3>"
  $ReportOutput += $OPMOSTCOMEVENT | Select-Object "Event Number", Publishername, TotalEvents | ConvertTo-Html -Fragment
  $ReportOutput += "<h3>Number of Perf Insertions Per Day</h3>"
  $ReportOutput += $OPPERFIPERD | Select-Object DaySampled, NumPerfPerDay | ConvertTo-Html -Fragment
  $ReportOutput += "<h3>Top 25 Perf Insertions by Object/Counter Name</h3>"
  $ReportOutput += $OPPERFIBYN | Select-Object objectname, countername, total | ConvertTo-Html -Fragment
  $ReportOutput += "<h3>Top 25 Alerts by Alert Count</h3>"
  $ReportOutput += $OPTOPALERTC | Select-Object AlertStringName, Name, AlertCount, AlertCountWithRepeatCount | ConvertTo-Html -Fragment



  # Get the alerts with a repeat count higher than the variable $RepeatCount
  $RepeatCount = 200

  $ReportOutput += "<br>"
  $ReportOutput += "<h3>Alerts with a Repeat Count higher than $RepeatCount</h3>"


  # Produce a table of all Open Alerts above the repeatcount and add it to the Report
  $ReportOutput += $Alerts | Where-Object {$_.RepeatCount -ge $RepeatCount -and $_.ResolutionState -ne 255} | Select-Object Name, Category, NetBIOSComputerName, RepeatCount | Sort-Object repeatcount -desc | ConvertTo-HTML -fragment

  #Output to HTML report
  $ReportOutput += "<h3>Stale State Change Data</h3>"
  $ReportOutput += $OPSTALESTD | Select-Object "Total StateChanges", "within grooming retention", "> grooming retention","> 30 days","> 90 days","> 365 days"| ConvertTo-Html -Fragment
  $ReportOutput += "<h3>Top 25 Monitors Changing State in the last 7 Days</h3>"
  $ReportOutput += $OPNOISYMON | Select-Object MonitorDisplayName, MonitorIdName, TargetClass, NumStateChanges | ConvertTo-Html -Fragment
  $ReportOutput += "<h3>Top 25 Monitors Changing State By Object</h3>"
  $ReportOutput += $OPNOISYMONOBJ | Select-Object ObjectName, Path, MonitorDisplayName, MonitorIdName,TargetClass, NumStateChanges | ConvertTo-Html -Fragment
  $ReportOutput += "<h3>Operations Database Grooming History</h3>"
  $ReportOutput += $OPDBGROOM | Select-Object ObjectName, GroomingSproc, DaysToKeep, GroomingRunTime,DataGroomedMaxTime | ConvertTo-HTML -Fragment

  # SQL Query to find out what Grooming Jobs have run in the last 24 hours
  $DidGroomingRun = @"
-- Did Grooming Run?:
SELECT [InternalJobHistoryId]
      ,[TimeStarted]
      ,[TimeFinished]
      ,[StatusCode]
      ,[Command]
      ,[Comment]
FROM [dbo].[InternalJobHistory]
WHERE [TimeStarted] >= DATEADD(day, -2, GETDATE())
Order by [TimeStarted]
"@


  # Produce a table of Grooming History and add it to the Report
  $ReportOutput += "<h3>Grooming History From The Past 48 Hours</h3>"
  $ReportOutput += Run-OpDBSQLQuery $DidGroomingRun InternalJobHistoryId, TimeStarted, TimeFinished, StatusCode, Command, Comment | Select-Object | ConvertTo-HTML -fragment

  #Produce Table of DW DB Health
  $ReportOutput +="<h2>Data Warehouse Database Health</h2>"
  $ReportOutput +="<h3>Data Warehouse DB Backup Status</h3>"
  $ReportOutput +=$DWDBBACKUP | Select-Object name, DaysSinceBackup | ConvertTo-Html -Fragment
  $ReportOutput +="<h3>Data Warehouse Top 25 Largest Tables</h3>"
  $ReportOutput +=$DWDBLARGETAB | Select-Object tablename, reserved, row_count, data, index_size, unused |ConvertTo-Html -fragment
  $ReportOutput +="<h3>Data Warehouse Staging Tables</h3>"
  $ReportOutput +=$DWDBSGTB | Select-Object "Alert Staging Table", Table | ConvertTo-Html -Fragment
  $ReportOutput +=$DWDBSGTBEV | Select-Object "Event Staging Table", Table | ConvertTo-Html -Fragment
  $ReportOutput +=$DWDBSGTBPE | Select-Object "Perf Staging Table", Table | ConvertTo-Html -Fragment
  $ReportOutput +=$DWDBSGTBST | Select-Object "State Staging Table", Table| ConvertTo-Html -Fragment
  $ReportOutput +="<h3>Data Warhouse Grooming Retention</h3>"
  $ReportOutput +=$DWDBGRET | Select-Object "Dataset Name", "Agg Type 0=raw, 20=Hourly, 30=Daily","Retention Time in Days"| ConvertTo-Html -Fragment

  # Insert the Results for the Number of Management Servers into the Report
  $ReportOutput += "<p>Number of Management Servers : $($ManagementServers.count)</p>"

  # Retrieve the data for the Management Servers
  $ReportOutput += "<br>"
  $ReportOutput += "<h2>Management Servers</h2>"

  $AllManagementServers = @()

  ForEach ($ManagementServer in $ManagementServers)
  {
    # Find out the Server Uptime for each of the Management Servers
    #Original query referenced -computer $ManagementServer.Name this was an error I modified to .Displayname to fix
    #TPaul: updated this to use CIM instead of WMI
    $lastboottime = (Get-CimInstance -ClassName win32_operatingsystem ).LastBootUpTime
    $sysuptime =New-TimeSpan $lastboottime (Get-Date)  
    $totaluptime = "" + $sysuptime.days + " days " + $sysuptime.hours + " hours " + $sysuptime.minutes + " minutes " + $sysuptime.seconds + " seconds"

    # Find out the Number of WorkFlows Running on each of the Management Servers
    $perfWorkflows = Get-Counter -ComputerName $ManagementServer.DisplayName -Counter "\Health Service\Workflow Count" -SampleInterval 1 -MaxSamples 1

    # The Performance Counter seems to return a lot of other characters and spaces...I only want the number of workflows, let's dump the rest
    [int]$totalWorkflows = $perfWorkflows.readings.Split(':')[-1] | ForEach-Object {$_.TrimStart()} | ForEach-Object {$_.TrimEnd()}

    $ManagementServerProperty = New-Object psobject
    $ManagementServerProperty | Add-Member noteproperty "Management Server" ($ManagementServer.DisplayName)
    $ManagementServerProperty | Add-Member noteproperty "Health State" ($ManagementServer.HealthState)
    $ManagementServerProperty | Add-Member noteproperty "Version" ($ManagementServer.Version)
    $ManagementServerProperty | Add-Member noteproperty "Action Account" ($ManagementServer.ActionAccountIdentity)
    $ManagementServerProperty | Add-Member noteproperty "System Uptime" ($totaluptime)
    $ManagementServerProperty | Add-Member noteproperty "Workflows" ($totalWorkflows)
    $AllManagementServers += $ManagementServerProperty
  }

  # Insert the Results for the Management Servers into the Report
  $ReportOutput += $AllManagementServers | Select-Object "Management Server", "Health State", "Version", "Action Account", "System Uptime", "Workflows" | Sort-Object "Management Server" | ConvertTo-HTML -fragment

  # Insert the Results for the Stats and State Changes into the Report
  $ReportOutput += "<br>"
  $ReportOutput += "<h2>Daily KPI</h2>"
  $ReportOutput += $AllStats | Select-Object "Open Alerts", "Groups", "Monitors", "Rules", "State Changes Yesterday" | ConvertTo-HTML -fragment

  # Retrieve and Insert the Results for the Management Packs that have been modified into the Report
  Write-Host "Checking for Management Packs that have been modified in the last 24 hours"

  $ReportOutput += "<br>"
  $ReportOutput += "<h2>Management Packs Modified in the Last 24 Hours</h2>"
  If (!($ManagementPacks | Where-Object {$_.LastModified -ge (Get-Date).addhours(-24)}))
  {
    $ReportOutput += "<p>No Management Packs have been updated in the last 24 hours</p>"
  }
  Else
  {
    $ReportOutput += $ManagementPacks | Where-Object {$_.LastModified -ge (Get-Date).addhours(-24)} | Select-Object Name, LastModified | ConvertTo-HTML -fragment
  }


  # Retrieve and Insert the Results for the Default Management Pack into the Report
  # This 'should be empty'...don't store stuff here!
  Write-Host "Checking for the Default Management Pack for Overrides"
  $ReportOutput += "<br>"
  $ReportOutput += "<h2>The Default Management Pack</h2>"

  # Excluding these 2 ID's as they are part of the default MP for DefaultUser and Language Code Overrides
  $excludedID = "5a67584f-6f63-99fc-0d7a-55587d47619d", "e358a914-c851-efaf-dda9-6ca5ef1b3eb7"
  $defaultMP = $ManagementPacks | Where-Object {$_.Name -match "Microsoft.SystemCenter.OperationsManager.DefaultUser"}
  ##Changed below line for compat with PowerShell 2.0
  ##If (!($defaultMP.GetOverrides() | ? {$_.ID -notin $excludedID}))
  If (!($defaultMP.GetOverrides() | Where-Object {$excludedID -NotContains $_.ID}))
  {
    $ReportOutput += "<p>There are no Overrides being Stored in the Default Management Pack</p>"
  }
  Else
  {

    ##Changed below line for compat with PowerShell 2.0
    #$foundOverride = Get-SCOMClassInstance -id ($defaultMP.GetOverrides() | ? {$_.ID -notin $excludedID -AND $_.ContextInstance -ne $guid} | select -expandproperty ContextInstance -ea SilentlyContinue)
    $foundOverride = Get-SCOMClassInstance -id ($defaultMP.GetOverrides() | Where-Object {$excludedID -NotContains $_.ID -AND $_.ContextInstance -ne $guid} | Select-Object -expandproperty ContextInstance -ea SilentlyContinue)


    $ReportOutput += "<p>Found overrides against the following targets: $foundOverride.Displayname</p>"
    ##PowerShell 2.0 Compat
    ##$ReportOutput += $($defaultMP.GetOverrides() | ? {$_.ID -notin $excludedID} | Select Name, Property, Value, LastModified, TimeAdded) | ConvertTo-HTML -fragment
    $ReportOutput += $($defaultMP.GetOverrides() | Where-Object {$excludedID -NotContains $_.ID} | Select-Object -Property Name, Property, Value, LastModified, TimeAdded) | ConvertTo-HTML -fragment

  }



  # Show all Agents that are in an Uninitialized State
  Write-Host "Checking for Uninitialized Agents"

  $ReportOutput += "<br>"
  $ReportOutput += "<h2>Uninitialized Agents</h2>"
  If (!($Agents | Where-Object -FilterScript {$_.HealthState -eq "Uninitialized"} | Select-Object -Property Name))
  {
    $ReportOutput += "<p>No Agents are in the Uninitialized State</p>"
  }
  Else
  {
    $ReportOutput += $Agents | Where-Object -FilterScript {$_.HealthState -eq "Uninitialized"} | Select-Object -Property Name | ConvertTo-HTML -fragment
  }


  # Show a Summary of all Agents States
  $healthy = $uninitialized = $warning = $critical = 0

  Write-Host "Checking Agent States"

  $ReportOutput += "<br>"
  $ReportOutput += "<h3>Agent Stats</h3>"

  switch ($Agents | Select-Object HealthState ) {
    {$_.HealthState -like "Success"} {$healthy++}
    {$_.HealthState -like "Uninitialized"} {$uninitialized++}
    {$_.HealthState -like "Warning"}  {$warning++}
    {$_.HealthState -like "Error"} {$critical++}
  }
  $totalagents = ($healthy + $warning + $critical + $uninitialized)

  $AllAgents = @()

  $iAgent = New-Object psobject
  $iAgent | Add-Member noteproperty -Name "Agents Healthy" -Value $healthy
  $iAgent | Add-Member noteproperty -Name "Agents Warning" -Value $warning
  $iAgent | Add-Member noteproperty -Name "Agents Critical" -Value $critical
  $iAgent | Add-Member noteproperty -Name "Agents Uninitialized" -Value $uninitialized
  $iAgent | Add-Member noteproperty -Name "Total Agents" -Value $totalagents

  $AllAgents += $iAgent

  $ReportOutput += $AllAgents | Select-Object -Property "Agents Healthy", "Agents Warning", "Agents Critical", "Agents Uninitialized", "Total Agents" | ConvertTo-HTML -fragment

  # Agent Pending Management States
  Write-Host "Checking Agent Pending Management States"

  $ReportOutput += "<br>"
  $ReportOutput += "<h3>Agent Pending Management Summary</h3>"

  $pushInstall = $PushInstallFailed = $ManualApproval = $RepairAgent = $RepairFailed = $UpdateFailed = 0

  $agentpending = Get-SCOMPendingManagement
  switch ($agentpending | Select-Object AgentPendingActionType ) {
    {$_.AgentPendingActionType -like "PushInstall"} {$pushInstall++}
    {$_.AgentPendingActionType -like "PushInstallFailed"} {$PushInstallFailed++}
    {$_.AgentPendingActionType -like "ManualApproval"}  {$ManualApproval++}
    {$_.AgentPendingActionType -like "RepairAgent"} {$RepairAgent++}
    {$_.AgentPendingActionType -like "RepairFailed"} {$RepairFailed++}
    {$_.AgentPendingActionType -like "UpdateFailed"} {$UpdateFailed++}

  }

  $AgentsPending = @()

  $AgentSummary = New-Object psobject
  $AgentSummary | Add-Member noteproperty "Push Install" ($pushInstall)
  $AgentSummary | Add-Member noteproperty "Push Install Failed" ($PushInstallFailed)
  $AgentSummary | Add-Member noteproperty "Manual Approval" ($ManualApproval)
  $AgentSummary | Add-Member noteproperty "Repair Agent" ($RepairAgent)
  $AgentSummary | Add-Member noteproperty "Repair Failed" ($RepairFailed)
  $AgentSummary | Add-Member noteproperty "Update Failed" ($UpdateFailed)

  $AgentsPending += $AgentSummary

  # Produce a table of all Agent Pending Management States and add it to the Report
  $ReportOutput += $AgentsPending | Select-Object "Push Install", "Push Install Failed", "Manual Approval", "Repair Agent", "Repair Failed", "Update Failed" | ConvertTo-HTML -fragment

  $ReportOutput += "<br>"
  $ReportOutput += "<h2>Alerts</h2>"

  $AlertsAll = ($Alerts | Where-Object {$_.ResolutionState -ne 255}).Count
  $AlertsWarning = ($Alerts | Where-Object {$_.Severity -eq "Warning" -AND $_.ResolutionState -ne 255}).Count
  $AlertsError = ($Alerts | Where-Object {$_.Severity -eq "Error" -AND $_.ResolutionState -ne 255}).Count
  $AlertsInformation = ($Alerts | Where-Object {$_.Severity -eq "Information" -AND $_.ResolutionState -ne 255}).Count
  $Alerts24hours = ($Alerts | Where-Object {$_.TimeRaised -ge (Get-Date).addhours(-24) -AND $_.ResolutionState -ne 255}).count

  $AllAlerts = @()


  $AlertSeverity = New-Object psobject
  $AlertSeverity | Add-Member noteproperty "Warning" ($AlertsWarning)
  $AlertSeverity | Add-Member noteproperty "Error" ($AlertsError)
  $AlertSeverity | Add-Member noteproperty "Information" ($AlertsInformation)
  $AlertSeverity | Add-Member noteproperty "Last 24 Hours" ($Alerts24hours)
  $AlertSeverity | Add-Member noteproperty "Total Open Alerts" ($AlertsAll)
  $AllAlerts += $AlertSeverity


  # Produce a table of all alert counts for warning, error, information, Last 24 hours and Total Alerts and add it to the Report
  $ReportOutput += $AllAlerts | Select-Object "Warning", "Error", "Information", "Last 24 Hours", "Total Open Alerts" | ConvertTo-HTML -fragment

  <#
      # Check if the Action Account is a Local Administrator on Each Management Server
      # This will only work if the account is a member of the Local Administrators Group directly.
      # If it has access by Group Membership, you can look for that Group instead
      # $ActionAccount = "YourGrouptoCheck"
      # Then replace all 5 occurrences below of $ManagementServer.ActionAccountIdentity with $ActionAccount
 
      Write-Host "Checking if the Action Account is a member of the Local Administrators Group for each Management Server"
 
      $ReportOutput += "<br>"
      $ReportOutput += "<h2>SCOM Action Account</h2>"
 
      ForEach ($ms in $ManagementServers.DisplayName | sort DisplayName)
      {
      $admins = @()
      $group =[ADSI]"WinNT://$ms/Administrators"
      $members = @($group.psbase.Invoke("Members"))
      $members | foreach {
      $obj = new-object psobject -Property @{
      Server = $Server
      Admin = $_.GetType().InvokeMember("Name", 'GetProperty', $null, $_, $null)
      }
      $admins += $obj
      }
 
      If ($admins.admin -match $ManagementServer.ActionAccountIdentity)
      {
      # Write-Host "The user $($ManagementServer.ActionAccountIdentity) is a Local Administrator on $ms"
      $ReportOutput += "<p>The user $($ManagementServer.ActionAccountIdentity) is a Local Administrator on $ms</p>"
      }
      Else
      {
      # Write-Host "The user $($ManagementServer.ActionAccountIdentity) is NOT a Local Administrator on $ms"
      $ReportOutput += "<p><span style=`"color: `#CC0000;`">The user $($ManagementServer.ActionAccountIdentity) is NOT a Local Administrator on $ms</span></p>"
      }
      }
  #>




  # Objects in Maintenance Mode

  #SQL Query Servers in MMode
  $ServersInMM =@"
select DisplayName from dbo.MaintenanceMode mm
join dbo.BaseManagedEntity bm on mm.BaseManagedEntityId = bm.BaseManagedEntityId
where Path is NULL and IsInMaintenanceMode = 1
"@


  $OpsDBSIMM = Run-OpDBSQLQuery $ServersInMM

  $ReportOutput += "<br>"
  $ReportOutput += "<h2>Servers in Maintenance Mode</h2>"

  If (!($OpsDBSIMM))
  {
    $ReportOutput += "<p>There are no objects in Maintenance Mode</p>"
  }
  Else
  {
    $ReportOutput += $OpsDBSIMM | Select-Object DisplayName | ConvertTo-HTML -fragment
  }

  <#
      # Global Grooming Settings
      # Simple comparisons against the values set at the beginning of this script
      # I use this to see if anyone has changed the settings. So set the values at the top of this script to match the values that your environment 'should' be set to.
 
      $ReportOutput += "<br>"
      $ReportOutput += "<h2>SCOM Global Settings</h2>"
 
 
      $SCOMDatabaseGroomingSettings = Get-SCOMDatabaseGroomingSetting
 
 
      If ($SCOMDatabaseGroomingSettings.AlertDaysToKeep -ne $AlertDaysToKeep)
      {$ReportOutput += "<p><span style=`"color: `#CC0000;`">Alert Days to Keep has been changed! Reset back to $AlertDaysToKeep</span></p>"}
      Else {$ReportOutput += "<p>Alert Days is correctly set to $AlertDaysToKeep</p>"}
 
      If ($SCOMDatabaseGroomingSettings.AvailabilityHistoryDaysToKeep -ne $AvailabilityHistoryDaysToKeep)
      {$ReportOutput += "<p><span style=`"color: `#CC0000;`">Availability History Days has been changed! Reset back to $AvailabilityHistoryDaysToKeep</span></p>"}
      Else {$ReportOutput += "<p>Availability History Days is correctly set to $AvailabilityHistoryDaysToKeep</p>"}
 
      If ($SCOMDatabaseGroomingSettings.EventDaysToKeep -ne $EventDaysToKeep)
      {$ReportOutput += "<p><span style=`"color: `#CC0000;`">Event Days has been changed! Reset back to $EventDaysToKeep</span></p>"}
      Else {$ReportOutput += "<p>Event Days is correctly set to $EventDaysToKeep</p>"}
 
      If ($SCOMDatabaseGroomingSettings.JobStatusDaysToKeep -ne $JobStatusDaysToKeep)
      {$ReportOutput += "<p><span style=`"color: `#CC0000;`">Job Days (Task History) has been changed! Reset back to $JobStatusDaysToKeep</span></p>"}
      Else {$ReportOutput += "<p>Job Days (Task History) is correctly set to $JobStatusDaysToKeep</p>"}
 
      If ($SCOMDatabaseGroomingSettings.MaintenanceModeHistoryDaysToKeep -ne $MaintenanceModeHistoryDaysToKeep)
      {$ReportOutput += "<p><span style=`"color: `#CC0000;`">Maintenance Mode History has been changed! Reset back to $MaintenanceModeHistoryDaysToKeep</span></p>"}
      Else {$ReportOutput += "<p>Maintenance Mode History is correctly set to $MaintenanceModeHistoryDaysToKeep</p>"}
 
      If ($SCOMDatabaseGroomingSettings.MonitoringJobDaysToKeep -ne $MonitoringJobDaysToKeep)
      {$ReportOutput += "<p><span style=`"color: `#CC0000;`">Monitoring Job Data has been changed! Reset back to $MonitoringJobDaysToKeep</span></p>"}
      Else {$ReportOutput += "<p>Monitoring Job Data is correctly set to $MonitoringJobDaysToKeep</p>"}
 
      If ($SCOMDatabaseGroomingSettings.PerformanceDataDaysToKeep -ne $PerformanceDataDaysToKeep)
      {$ReportOutput += "<p><span style=`"color: `#CC0000;`">Performance Data has been changed! Reset back to $PerformanceDataDaysToKeep</span></p>"}
      Else {$ReportOutput += "<p>Performance Data is correctly set to $PerformanceDataDaysToKeep</p>"}
 
      If ($SCOMDatabaseGroomingSettings.StateChangeEventDaysToKeep -ne $StateChangeEventDaysToKeep)
      {$ReportOutput += "<p><span style=`"color: `#CC0000;`">State Change Data has been changed! Reset back to $StateChangeEventDaysToKeep</span></p>"}
      Else {$ReportOutput += "<p>State Change Data is correctly set to $StateChangeEventDaysToKeep</p>"}
 
 
      # SCOM Agent Heartbeat Settings
      $HeartBeatSetting = Get-SCOMHeartbeatSetting
 
      If ($HeartBeatSetting.AgentHeartbeatInterval -ne 180 -AND $HeartBeatSetting.MissingHeartbeatThreshold -ne 3)
      {$ReportOutput += "<p><span style=`"color: `#CC0000;`">The HeartBeat Settings have been changed! Reset back to $AgentHeartbeatInterval and $MissingHeartbeatThreshold</span></p>"}
      Else {$ReportOutput += "<p>The HeartBeat Settings are correctly set to Interval: $AgentHeartbeatInterval and Missing Threshold: $MissingHeartbeatThreshold</p>"}
  #>

  # How long did this script take to run?
  $EndTime=Get-Date
  $TotalRunTime=$EndTime-$StartTime

  # Add the time to the Report
  $ReportOutput += "<br>"
  $ReportOutput += "<p>Total Script Run Time: $($TotalRunTime.hours) hrs $($TotalRunTime.minutes) min $($TotalRunTime.seconds) sec</p>"

  # Close the Body of the Report
  $ReportOutput += "</body>"

  Write-Host "Saving HTML Report to $ReportPath"

  # Save the Final Report to a File
  ConvertTo-HTML -head $Head -body "$ReportOutput" | Out-File $ReportPath

  Invoke-Item "$ReportPath"

  <#
      # Send Final Report by email...
 
      Write-Host "Emailing Report"
      $SMTPServer ="your.smtpserver.com"
      $Body = ConvertTo-HTML -head $Head -body "$ReportOutput"
      $SmtpClient = New-Object Net.Mail.SmtpClient($smtpServer);
      $mailmessage = New-Object system.net.mail.mailmessage
      $mailmessage.from = "sender@company.com"
      $mailmessage.To.add("recipient@company.com")
      # Want more recipient's? Just add a new line
      $mailmessage.To.add("anotherrecipient@company.com")
      $mailmessage.Subject = "Tims SCOM Healthcheck Report"
      $MailMessage.IsBodyHtml = $true
      $mailmessage.Body = $Body
      $smtpclient.Send($mailmessage)
  #>


  <#
      # Insert the Results for the Reporting Server URL into the Report
      $ReportOutput += "<h2>Reporting Server</h2>"
      $ReportOutput += "<p>Reporting Server URL : <a href=`"$ReportingURL/`">$ReportingURL</a></p>"
      $ReportOutput += "<p>The Reporting Server URL Status : $ReportingServerStatus</p>"
 
      # Insert the Results for the Web Console URL into the Report
      $ReportOutput += "<h2>Web Console Servers</h2>"
      $ReportOutput += "<p>Web Console URL : <a href=`"$WebConsoleURL/`">$WebConsoleURL</a></p>"
      $ReportOutput += "<p>The Web Console URL Status : $WebConsoleStatus</p>"
  #>


  # A bit of cleanup
  #Clear-Variable Agents, Alerts, Groups, ManagementGroup, ManagementPacks, ManagementServer, Monitors, Rules, ReportOutput, StartTime, EndTime, TotalRunTime, SCOMDatabaseGroomingSettings

}
#######################################################################

<#
    .Synopsis
    Will unseal MP and MPB files along with any additional resources contained therein and place into version-coded folders.
    .EXAMPLE
    PS C:\> Unseal-SCOMMP -inDir C:\MyMPs -outDir C:\MyMPs\UnsealedMPs
    .EXAMPLE
    PS C:\> Unseal-SCOMMP -inDir 'C:\Program Files (x86)\System Center Management Packs' -outDir 'C:\Temp\Unsealtest_OUT'
    .EXAMPLE
    PS C:\> Unseal-SCOMMP -inDir 'C:\en_system_center_2012_r2_operations_manager_x86_and_x64_dvd_2920299\ManagementPacks' -outDir 'C:\Temp\Unsealtest_OUT'
    .INPUTS
    Strings (directory paths)
    .OUTPUTS
    XML files (+ any resource files: .dll, .bmp, .ico etc.)
 
    .NOTES
    Author (Revised by): Tyson Paul (I modified this heavily but the original that I used as a framework was not written by me.)
    Original Author: I can't seem to find where I originally got this script from. Jon Almquist maybe?
    Blog: https://blogs.msdn.microsoft.com/tysonpaul/2018/07/18/scomhelper-powershell-module-a-scom-admins-best-friend/
    History:
      2019.05.01 - Fixed issue where output path would display extra backslash
      2018.05.25 - Initial.
    .LINK
    Get-SCOMMPFileInfo
    Get-SCOMClassInfo
#>

Function Unseal-SCOMMP {
  Param(
    $inDir="C:\Temp\Sealed",
    $outDir="C:\Temp\AllUnsealed"
  )

  [Reflection.Assembly]::LoadWithPartialName("Microsoft.EnterpriseManagement.Core")
  [Reflection.Assembly]::LoadWithPartialName("Microsoft.EnterpriseManagement.Packaging")

  $mpStore = New-Object Microsoft.EnterpriseManagement.Configuration.IO.ManagementPackFileStore($inDir)
  $mps = Get-ChildItem $inDir *.mp -Recurse
  If ($null -ne $mps) {
    ForEach ($file in $mps) {
      $MPFilePath = $file.FullName
      $mp = New-Object Microsoft.EnterpriseManagement.Configuration.ManagementPack($MPFilePath)
      $newOutDirPath = (Join-Path $outDir  ($file.Name + "($($mp.Version.ToString()))") )
      New-Item -Path $newOutDirPath -ItemType Directory -ErrorAction SilentlyContinue
      Write-Host $file
      $mpWriter = New-Object Microsoft.EnterpriseManagement.Configuration.IO.ManagementPackXmlWriter( $newOutDirPath )
      $mpWriter.WriteManagementPack($mp)
    }
  }

  #now dump MPB files
  $mps = Get-ChildItem $inDir *.mpb -Recurse
  $mpbReader = [Microsoft.EnterpriseManagement.Packaging.ManagementPackBundleFactory]::CreateBundleReader()
  If ($null -ne $mps) {
    ForEach ($mp in $mps) {
      $bundle = $mpbReader.Read($mp.FullName, $mpStore)
      $newOutDirPath = (Join-Path $outDir ($mp.Name + "($($bundle.ManagementPacks.Version.ToString()))" ))
      New-Item -Path $newOutDirPath -ItemType Directory -ErrorAction SilentlyContinue
      Write-Host $mp
      $mpWriter = New-Object Microsoft.EnterpriseManagement.Configuration.IO.ManagementPackXmlWriter($newOutDirPath)
      ForEach($bundleItem in $bundle.ManagementPacks) {
        #write the xml
        $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 $newOutDirPath ('.' + $stream + '.bin'))
          }
          Else {
            If ($fileName.IndexOf('\') -gt 0) {
              #$outpath = Split-Path $fileName -Leaf
              $outpath = (Join-Path $newOutDirPath (Split-Path $fileName -Leaf))
            }
            Else {
              $outpath = (Join-Path $newOutDirPath $fileName)
            }
          }
          Write-Host "`t$outpath"
          $fs = [system.io.file]::Create($outpath)
          $streams[$stream].WriteTo($fs)
          $fs.Close()
        }
      }
    }
  }

}#End Function
#######################################################################

<#
    .Synopsis
    List version and file path information for all management pack files (*.mp, *.mpb, *.xml).
    .DESCRIPTION
    Will display MP file and version information.
 
    .EXAMPLE
    Get-SCOMMPFileInfo -inDir 'C:\Program Files (x86)\System Center Management Packs' -Verbose
 
    .EXAMPLE
    Get-SCOMMPFileInfo -inDir 'C:\Management Packs' -GridView
 
    .INPUTS
    System.String, Switch
    .OUTPUTS
    Custom object
    .NOTES
    Author: Tyson Paul
    Blog: https://blogs.msdn.microsoft.com/tysonpaul/2018/07/18/scomhelper-powershell-module-a-scom-admins-best-friend/
    Date: 2017.06.01
    Version: v1.0
    History:
    2018.05.30: Added GridView option as well as some other cleanup.
    .FUNCTIONALITY
    Useful for identifying specific versions of management packs in your local archive.
    .LINK
    Get-SCOMClassInfo
    New-SCOMClassGraph
    Unseal-SCOMMP
#>

Function Get-SCOMMPFileInfo {

  [CmdletBinding(DefaultParameterSetName='Parameter Set 1',
      SupportsShouldProcess=$true,
  PositionalBinding=$false)]
  Param (
    [Parameter(Mandatory=$true,
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false,
        ValueFromRemainingArguments=$false,
        Position=0,
    ParameterSetName='Parameter Set 1')]
    [ValidateNotNull()]
    [ValidateNotNullOrEmpty()]
    [Alias("Path")]
    [string]$inDir,

    [switch]$GridView=$true,
    [switch]$passthru=$true
  )
  $MPs = @()
  $arrMaster = @()

  $MPFiles = Get-ChildItem -Path $inDir -Recurse -Include *.mp,*.mpb,*.xml
  ForEach ($File in $MPFiles.FullName) {
    Switch ($File) {
      {$_ -Match '\.mp$|\.xml$'} {
        $MP = Get-SCManagementPack -ManagementPackFile $File
        break;
      }

      {$_ -Match '\.mpb$'} {
        $MP = Get-SCManagementPack -BundleFile $File
        break;
      }
    }#end Switch
    $obj = New-Object PSCustomObject
    $obj | Add-Member -Type NoteProperty -Name Name -Value $MP.Name
    $obj | Add-Member -Type NoteProperty -Name DisplayName -Value $MP.DisplayName
    $obj | Add-Member -Type NoteProperty -Name Version -Value $MP.Version
    $obj | Add-Member -Type NoteProperty -Name Description -Value $MP.Description
    $obj | Add-Member -Type NoteProperty -Name FullName -Value $File
    $obj | Add-Member -Type NoteProperty -Name ID -Value $MP.ID.GUID
    Write-Verbose "$File"
    $arrMaster += $obj
  }#end ForEach
  If ($GridView) {
    $arrMaster | Sort-Object Name, Version | Out-GridView -PassThru:$passthru
  }
  Else{
    $arrMaster | Sort-Object Name, Version
  }
}#End Function
#######################################################################

<#
    .Synopsis
    Will display statistics about the total number of SCOM class instances and the management packs from which they originate.
    .EXAMPLE
    Get-SCOMClassInfo -Top 10
    .EXAMPLE
    Get-SCOMClassInfo -Top 30 -MPStats
    .EXAMPLE
    Get-SCOMClassInfo -MPStats -ShowGraph
    .INPUTS
    None
    .OUTPUTS
    Custom Object: {InstanceCount,ClassName,MPName}, {MPName,TotalInstances}
    .NOTES
    Author: Tyson Paul
    Blog: https://blogs.msdn.microsoft.com/tysonpaul/2018/07/18/scomhelper-powershell-module-a-scom-admins-best-friend/
    Version: 1.0
    Original Date: 2018.05.25
    .FUNCTIONALITY
    Statistical reporting.
    .LINK
    Get-SCOMMPFileInfo
    New-SCOMClassGraph
    Unseal-SCOMMP
#>

Function Get-SCOMClassInfo {
  Param (
    #Will return top N results by InstanceCount
    [int]$Top=30,
    [switch]$MPStats,
    [switch]$ShowGraph
  )
  # Get all MPs that were not installed OotB.
  $MPs = Get-SCManagementPack
  $MPs = $MPs | Sort-Object TimeCreated | Select-Object -Last ($MPs.Count - 98)
  $Classes = Get-SCClass -ManagementPack $MPs | Where-Object {$_.Singleton -ne $true}
  #$arrClasses = @()
  ForEach ($Class in $Classes) {
    $obj = New-Object PSCustomObject
    $Instances = $Class | Get-SCOMClassInstance
    $obj | Add-Member -MemberType NoteProperty -Name Instances -Value $Instances.Count
    $obj | Add-Member -MemberType NoteProperty -Name ClassName -Value $Class.Name
    $obj | Add-Member -MemberType NoteProperty -Name MPName -Value $Class.ManagementPackName
    [System.Object[]]$arrClasses += $obj
  }
  If ($MPStats){
    $Results = (($arrClasses | Group-Object -Property MPName ) | Select-Object @{Name='MPName';E={$_.Name} },@{Name='TotalInstances';E={($_.Group.Instances | Measure-Object -Sum).Sum}} |Sort-Object TotalInstances -Descending | Select-Object -First $Top)
    If ([bool]$ShowGraph) {
      $Results | Out-ConsoleGraph -Property TotalInstances -Columns TotalInstances,MPName
    }
    Else {
      Return $Results
    }
  }
  Else{
    $Results = ($arrClasses | Sort-Object Instances -Descending | Select-Object -First $Top)
    If ([bool]$ShowGraph) {
      $Results | Out-ConsoleGraph -Property Instances -Columns Instances,ClassName
    }
    Else {
      Return $Results
    }
  }
} #End Function

#######################################################################
<#
    .Synopsis
    Will disable all event collection rules for the provided sealed management pack(s).
    .DESCRIPTION
    Feed this function one or more management packs and it will create a corresponding unsealed pack for each in which it will store overrides which disable any/all event collection rules that are currently enabled.
    The resulting overrides will target the workflow class target.
    .EXAMPLE
    PS C:\> #
    PS C:\> # Create an array of MP names
    PS C:\> $names=@"
    Microsoft.SystemCenter.2007
    Microsoft.Windows.Server.2012.Monitoring
    Microsoft.Windows.Server.2012.R2.Monitoring
    Microsoft.Windows.Server.AD.2012.R2.Monitoring
    Microsoft.SystemCenter.Apm.Infrastructure.Monitoring
    Microsoft.SystemCenter.ACS.Internal
    Microsoft.Windows.Server.DNS
    "@ -split "\n" -replace "`r",""
 
    PS C:\> # Retrieve array of management packs. Pipe MPs to function.
    PS C:\> Get-SCOMManagementPack -Name $names | Disable-SCOMAllEventRules
 
    .EXAMPLE
    PS C:\> Disable-SCOMAllEventRules -ManagementPack (Get-SCOMManagementPack -Name 'Microsoft.Windows.Server.2012.Monitoring')
 
    .EXAMPLE
    PS C:\> Disable-SCOMAllEventRules -ManagementPack (Get-SCOMManagementPack -Name @('Microsoft.SQLServer.2012.Mirroring.Discovery','Microsoft.SQLServer.2008.Discovery') )
    .INPUTS
    Operations Manager Management Pack object
    .OUTPUTS
    Status messages (strings)
    .NOTES
    Author: Tyson Paul
    Blog: https://blogs.msdn.microsoft.com/tysonpaul/2018/07/18/scomhelper-powershell-module-a-scom-admins-best-friend/
    Date: 2018.06.26
    Version: 1.1
#>

Function Disable-SCOMAllEventRules {

  Param (

    [Parameter(Mandatory=$true,
        ValueFromPipeline=$true,
        ValueFromPipelineByPropertyName=$false,
        ValueFromRemainingArguments=$false,
        Position=0,
    ParameterSetName='Parameter Set 2')]
    [ValidateNotNull()]
    [ValidateNotNullOrEmpty()]
    [ValidateScript({$_.Sealed -eq $true})]
    [Alias('ManagementPack')]
    [Microsoft.EnterpriseManagement.Configuration.ManagementPack[]]$MP,


    [string]$MgmtServer = 'Localhost',
    [string]$LogFile
  )

  #region Begin
  Begin{

    #---------------------------------------------------------------
    <#
        This function will help to organize properties of a custom object in the preferred order
    #>

    Function Combine-Objects {
      Param (
        [Parameter(mandatory=$true)]$Obj1,
        [Parameter(mandatory=$true)]$Obj2
      )

      $hash = @{}

      ForEach ( $Property in $Obj1.psobject.Properties) {
        $hash[$Property.Name] = $Property.value
      }

      ForEach ( $Property in $Obj2.psobject.Properties) {
        $hash[$Property.Name] = $Property.value
      }
      $Object3 = New-Object -TypeName PSObject -Prop $hash
      #$Object3 = [Pscustomobject]$arguments
      Return $Object3
    }
    #---------------------------------------------------------------
    <#
        This will create the necessary unsealed MP in which to store overrides
    #>

    Function Verify-OverrideMP {
      #If override pack does not already exist, create it
      If (-NOT [bool](Get-SCManagementPack -Name $OverridesMP['Name'])) {
        Write-Host "Override pack does not exist. Name: $($OverridesMP['Name'])"
        Write-Host "Attempt to create unsealed MP: $($OverridesMP['Name'])"
        $MPCreated = New-OMManagementPack -SDK $MgmtServer -Name $OverridesMP['Name'] -DisplayName $OverridesMP['DisplayName'] -Description "Bulk disabling of Event Collections."
        If ($MPCreated) {
          Write-Host "Success! New ManagementPack:" -F Green
          Write-Host "DisplayName: " -NoNewline
          Write-Host "$($OverridesMP['DisplayName'])" -F Cyan
          Write-Host "Name: $($OverridesMP['Name'])"
          Return $true
        }
        Else {
          Write-Host "Failed to create override pack with name: $($OverridesMP['Name']). Aborting."
          Return $false
        }
      }
      Else {
        Return $true
      }
    }
    #---------------------------------------------------------------

    $USERNAME = whoami
    $MPs = $MP

    # make sure a connection to an SDK exists
    If (-NOT [bool](Get-SCManagementGroupConnection) ) {
      $mg = New-SCManagementGroupConnection -ComputerName $MgmtServer
    }
    Import-Module OpsMgrExtended -ErrorAction SilentlyContinue

    # Make sure required module is available/installed
    If (-Not [bool](Get-Module -Name 'OpsMgrExtended' -ErrorAction SilentlyContinue )) {
      Write-Error "Required module 'OpsMgrExtended' does not exist. Please install module."
      $choice = Read-Host "Install module: OpsMgrExtended (Y/N) ?"
      While ($choice -notmatch '[y]|[n]'){
        $choice = Read-Host "Y/N?"
      }
      Switch ($choice){
        'y' {
          # Install module from the Powershell Gallery
          Write-Host "Find-Module OpsMgrExtended | Install-Module -Verbose"
          Find-Module OpsMgrExtended | Install-Module -Verbose
          If ($?){
            Write-Host "Module installed!"
            Import-Module OpsMgrExtended
          }
          Else {
            Write-Error "Problem installing module.
              You may manually download from this location: 'https://github.com/tyconsulting/OpsMgrExtended-PS-Module'. Additional information here: 'http://blog.tyang.org/2015/06/24/automating-opsmgr-part-1-introducing-opsmgrextended-powershell-sma-module/ , https://www.powershellgallery.com/packages/OpsMgrExtended/1.3.0'.
            For information on how to install a PowerShell module, see this article: 'https://msdn.microsoft.com/en-us/library/dd878350(v=vs.85).aspx'.`nExiting.`n"
;
            Break
          }
        }
        'n' {
          Write-Host "Module will not be installed.`nExiting.`n"
          Break
        }
      }
    }

    # If Logfile path not provided, set path to user temp directory.
    If (-NOT $LogFile ){
      $LogFile = Join-Path $env:TEMP "Disable-SCOMAllEventRules.csv"
    }
    If (-NOT (Test-Path $LogFile -PathType Leaf)){
      New-Item -Path $LogFile -ItemType File -Force | Out-Null
    }

  }#endregion Begin

  #region
  Process{
    ForEach ($MP in $MPs) {
      # These are the event collection write actions to focus on
      $WA_Collections = "Microsoft.SystemCenter.CollectEvent","Microsoft.SystemCenter.DataWarehouse.PublishEventData"
      Write-Host "Getting Event Collection rules from MP: $($MP.Name). This may take a minute ..."

      # Get any event collection rules from the MP
      $EventRules = Get-SCOMRule -ManagementPack $MP | Where-Object  {
        ([System.Object[]]$_.WriteActionCollection.TypeID.Identifier.Path -contains $WA_Collections[0]) `
        -OR `
        ([System.Object[]]$_.WriteActionCollection.TypeID.Identifier.Path -contains $WA_Collections[1]) `
      }
      Write-Host "$($EventRules.Count) Event Collection rules found."

      $OverridesMP = @{}
      $OverridesMP['Name'] = ($MP.Name.ToString())+".DisabledEventCollection.OVERRIDES"
      $OverridesMP['DisplayName'] = ($MP.DisplayName.ToString())+" DisabledEventCollection (OVERRIDES)"
      [int]$i=1

      ForEach ($thisRule in $EventRules) {
        Write-Progress -Activity "Creating Overrides" -Status "Evaluating rule status..." -PercentComplete ($i / $EventRules.Count *100)
        [bool]$Disabled = $false
        $Overrides = Get-SCOMOverride -Rule $thisRule
        #Determine if rule is 'Disabled" by default or already disabled via override. If so, skip.
        ForEach ($thisOverride in $Overrides) {
          If ( ($thisRule.Enabled -eq 'false') -OR ( ($thisOverride.Property -eq 'Enabled') -AND ($thisOverride.Value -eq 'false') )) {
            $Disabled=$true
          }
        }
        # If the rule is a valid candidate to be disabled, proceed to create override.
        If (-NOT $Disabled) {

          $ORName = "$($OverridesMP['Name']).OverrideForRule"+"$($thisRule.Name.Replace('.',''))ForContext$($thisRule.Target.Identifier.Path.Replace('.',''))"
          If ( $ORName.Length -gt 256) {
            $ORName = $ORName.Substring(0,255)
          }

          #Create a hashtable to use for splat
          $Params = @{
            sDK = $MgmtServer
            MPName = $OverridesMP['Name']
            OverrideType = 'RulePropertyOverride'
            OverrideName = $ORName
            OverrideWorkflow = $thisRule.Name
            Target = [string]$thisRule.Target.Identifier.Path
            OverrideProperty = 'Enabled'
            OverrideValue = 'False'
            Enforced = $false
            IncreaseMPVersion=$true
          }
          # Create timestamp for logging later on
          $now = (Get-Date -F yyyyMMdd-hhmmss)

          # Verify the necessary unsealed MP exists
          If (-NOT (Verify-OverrideMP) ) {
            Return
          }
          # Splat. Create the override
          $Result = New-OMPropertyOverride @Params

          # Create custom object. This will make it easy to pipe out data to a log file (csv)
          $object = New-Object PSCustomObject
          $object | Add-Member -MemberType NoteProperty -Name '#' -Value $i
          $object | Add-Member -MemberType NoteProperty -Name 'Date' -Value $now

          If ($Result) {
            $object | Add-Member -MemberType NoteProperty -Name 'Status' -Value 'SUCCESS'
          }
          Else {
            $object | Add-Member -MemberType NoteProperty -Name 'Status' -Value 'FAILED'
          }
          $object | Add-Member -MemberType NoteProperty -Name 'User' -Value $USERNAME
          $objParams = New-Object -TypeName PSObject -Prop $Params
          $object = Combine-Objects -Obj1 $object -Obj2 $objParams
          $object | Select-Object -Property "Date","Status","User","SDK","OverrideName","OverrideWorkflow","OverrideType","OverrideProperty","OverrideValue","Target","MPName","IncreaseMPVersion","Enforced" | Export-Csv -Path $LogFile -NoTypeInformation -Append
          $object | Select-Object  -Property "#",@{N='Disabled';E={$_.Status}},OverrideWorkflow | Format-Table -AutoSize
          Remove-Variable -Name Params,Result,object,objParams
          $i++
        }
      }
    }
  }#endregion Process

  End {
    Write-Host "`nLogfile: " -NoNewline
    Write-Host "$LogFile`n" -F Yellow
  }

}#end Function


#######################################################################
<#
    .SYNOPSIS
    Will ping all entries in your HOSTS file (C:\WINDOWS\System32\drivers\etc\hosts) very quickly.
 
    .EXAMPLE
    PS C:\> Ping-AllHosts -Count 30
 
    Will ping all hosts a total of 30 times, pausing for the default 10 seconds inbetween iterations.
    .EXAMPLE
    PS C:\> Ping-AllHosts -t -DelayMS 30000
 
    Will continue to ping hosts waiting 30 seconds inbetween iterations.
    .EXAMPLE
    PS C:\> Ping-AllHosts -Count 10
 
    Will ping all hosts a total of 10 times.
    .NOTES
    Name: Ping-AllHosts
    Author: Tyson Paul
    Blog: https://blogs.msdn.microsoft.com/tysonpaul/2018/07/18/scomhelper-powershell-module-a-scom-admins-best-friend/
    Date: 2018.04.30
    History: v1.0
 
#>

Function Ping-AllHosts {
  Param (
    [long]$Count=1,
    [long]$DelayMS=10000,
    [switch]$t
  )

  $HOSTSfile = (Join-Path $env:SystemRoot '\System32\drivers\etc\hosts')
  If (-NOT (Test-Path $HOSTSfile -PathType Leaf) ) {
    Write-Error "Cannot find 'hosts' file at: $HOSTSFile . Exiting."
    Return
  }
  If ($t) {
    $Count = 9999999999999
    Write-Host "`$Count = $Count" -ForegroundColor Yellow

  }
  [long]$i=1
  While ($i -le $Count) {
    Write-Host "Attempt: $i. Remaining attempts: $($Count - $i)" -F Green
    # Parse the Hosts file, extract only the lines that do NOT begin with hash (#). Discard any leading tabs or spaces on all lines. Ignore blank lines
    $lines = (( (Get-Content $HOSTSfile).Replace("`t",'#').Replace(" ",'#').Replace("##",'#') | Where-Object {$_ -match '^[^#]'}))  | Where-Object {$_.Length -gt 1}

    $hash = @{}
    # Assign names and IPs to hash table
    $lines | ForEach-Object { $hash[$_.Split('#')[1]] = $_.Split('#')[0] }
    If (-Not [bool]$lines) {
      Write-Host "No valid hosts found in file. Exiting."
      Return
    }
    # Perform simultaneous address connection testing, as job.
    $job = Test-Connection -ComputerName ($hash.Keys | Out-String -stream) -Count 1 -AsJob

    Do{
      Write-Host 'Waiting for ping test to complete...' -F Yellow
      Start-Sleep 3

    }While($job.JobStateInfo.State -eq "Running")
    Receive-Job $job |
    Select-Object @{N="Source"; E={$_.PSComputerName}},
              @{N="Target"; E={$_.Address}},
              IPV4Address,
              @{N='Time(ms)';E={($_.'ResponseTime')} },
              @{N='Alive?';E={([bool]$_.'ResponseTime')} } |
                  Sort-Object Target |
                         Format-Table -AutoSize

    If ($i -eq $Count) {Return}
    $i++
    Write-Host "Sleeping $($DelayMS / 1000) Seconds before next ping test..."
    Start-Sleep -Milliseconds $DelayMS
  }
}#end Function

#######################################################################

. $PSScriptRoot\Start-SCOMOverrideTool.ps1

$Functions = @'
Clear-SCOMCache
Disable-SCOMAllEventRules
Export-EffectiveMonitoringConfiguration
Export-SCOMEventsToCSV
Export-SCOMKnowledge
Get-SCOMAlertKnowledge
Get-SCOMClassInfo
Get-SCOMHealthCheckOpsConfig
Get-SCOMMPFileInfo
Get-SCOMRunAsAccountName
Get-StringHash
New-SCOMClassGraph
New-SCOMClassGraphIndex
Ping-AllHosts
Remove-SCOMObsoleteReferenceFromMPFile
Set-SCOMMPAliases
Show-SCOMModules
Test-Port
Show-SCOMPropertyBag
Start-SCOMOverrideTool
Unseal-SCOMMP
'@
 -split  "\n" -replace "`r","" | Export-ModuleMember