Public/Export-SCOMKnowledge.ps1

Function Export-SCOMKnowledge {
  <#
      .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
      #Export SCOM Knowledge for rules/monitors
      Get-SCOMManagementGroupConnection | Remove-SCOMManagementGroupConnection -Verbose
      New-SCOMManagementGroupConnection -ComputerName YOUR_SERVERNAME
      $SQLMPs = Get-SCOMManagementPack -Name *sql*
      $Outfolder = 'D:\SCOMReports'
 
      # Workflows to separate files
      ForEach ($MP in $SQLMPs){
      $rules = $MP.GetRules().Count
      $mons = $MP.GetMonitors().Count
      If (($rules + $mons) -gt 0){
      Export-SCOMKnowledge -OutFolder $Outfolder -ManagementPack $MP -Topic "$($MP.Name)_"
      }
      Else{
      Write-Host "No monitor/rule workflows found: " -NoNewline;
      Write-Host "$($MP.DisplayName)" -f Yellow
      }
      }
 
      #All MPs combined
      Export-SCOMKnowledge -OutFolder $Outfolder -ManagementPack $SQLMPs -Topic "ALL_SQL_"
 
 
      .LINK
      https://monitoringguys.com/
 
      .NOTES
      Author: Tyson Paul
      Blog: https://monitoringguys.com/
 
      History:
      2022.06.20: Used arraylist to improve speed when gathering rules/mons. Leveraged new Convert-MAML2HTML function for article conversion.
      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
 
  #>


  [CmdletBinding(DefaultParameterSetName='Parameter Set 1',
      SupportsShouldProcess=$false,
      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')]
    [Alias("WorkflowFilter")]
    [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)
          $articleContent = ($MamlText | ForEach-Object { Convert-MAMLToHtml -XML $_ })
        }

        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 = $NULL
        Try {
          $article = $thisWF.GetKnowledgeArticle($cultureInfo)
        } Catch {
          #Empty catch is fine here
        }

        If (-NOT $article){
          $error.Remove($Error[0])
          $articlecontent = "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 = priv_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 = priv_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 priv_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 priv_Invoke-CLSqlCmd
    #------------------------------------------------------------------------------------


    #####################################################################################
    $ThisScript = $MyInvocation.MyCommand.Path
    [System.Collections.ArrayList]$Rules=@()
    [System.Collections.ArrayList]$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-SCOMPowerShellModule

    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
          ( (Get-SCOMRule -ManagementPack $ManagementPack | Where-Object {( $_.Name -match "$ThisRegex") -OR ( $_.DisplayName -match "$ThisRegex")} ) | ForEach-Object {$NULL = $Rules.Add($_) })
          Write-Host "Getting all filtered Monitors..." -ForegroundColor Yellow
          ( (Get-SCOMMonitor -ManagementPack $ManagementPack | Where-Object {( $_.Name -match "$ThisRegex") -OR ( $_.DisplayName -match "$ThisRegex")} )  | ForEach-Object {$NULL = $Monitors.Add($_) })
        }
      }
      # If no filter(s) exist, then return all rules/mons from the designated MP.
      Else {
        Write-Host "Getting ALL Rules..." -ForegroundColor Yellow
        ( (Get-SCOMRule -ManagementPack $ManagementPack ) | ForEach-Object {$NULL = $Rules.Add($_) })
        Write-Host "Getting ALL Monitors..." -ForegroundColor Yellow
        ( (Get-SCOMMonitor -ManagementPack $ManagementPack )  | ForEach-Object {$NULL = $Monitors.Add($_) })
      }
    }
    # 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
          ( (Get-SCOMRule | Where-Object {( $_.Name -match "$ThisRegex") -OR ( $_.DisplayName -match "$ThisRegex")}) | ForEach-Object {$NULL = $Rules.Add($_) })
          Write-Host "Getting all filtered Monitors..." -ForegroundColor Yellow
          ( (Get-SCOMMonitor | Where-Object {( $_.Name -match "$ThisRegex") -OR ( $_.DisplayName -match "$ThisRegex")} ) | ForEach-Object {$NULL = $Monitors.Add($_) })
        }
      }
      Else{
        Write-Host "Getting ALL Rules..." -ForegroundColor Yellow
        ( (Get-SCOMRule) | ForEach-Object {$NULL = $Rules.Add($_) })
        Write-Host "Getting ALL Monitors..." -ForegroundColor Yellow
        ( (Get-SCOMMonitor) | ForEach-Object {$NULL = $Monitors.Add($_) })
      }
    }

  } #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 -Encoding UTF8
    }
    Else {
      $RulesTempContent = $myRulesObj | ConvertTo-HTML -Title "SCOM Rules" -Head $cssHead
      $RulesTempContent =  CleanHTML $RulesTempContent
      $RulesTempContent = $RulesTempContent -Replace '<table>', '<table border="1" cellpadding="20">'
      $RulesTempContent | Set-Content (Join-Path $OutFolder ($Topic +"Rules.html")) -Encoding UTF8
    }
    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 -Encoding UTF8
    }
    Else {
      $MonitorTempContent = $myMonitorObj | ConvertTo-HTML  -Title "SCOM Monitors" -Head $cssHead
      $MonitorTempContent =  CleanHTML $MonitorTempContent
      $MonitorTempContent = $MonitorTempContent -Replace '<table>', '<table border="1" cellpadding="20">'
      $MonitorTempContent | Set-Content (Join-Path $OutFolder ($Topic + "Monitors.html")) -Encoding UTF8
    }
    Write-host "Output folder: $OutFolder" -BackgroundColor Black -ForegroundColor Yellow

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