Monitoring.psm1

$script:ModuleRoot = $PSScriptRoot
$script:ModuleVersion = (Import-PowerShellDataFile -Path "$($script:ModuleRoot)\Monitoring.psd1").ModuleVersion

# Detect whether at some level dotsourcing was enforced
$script:doDotSource = Get-PSFConfigValue -FullName Monitoring.Import.DoDotSource -Fallback $false
if ($Monitoring_dotsourcemodule) { $script:doDotSource = $true }

<#
Note on Resolve-Path:
All paths are sent through Resolve-Path/Resolve-PSFPath in order to convert them to the correct path separator.
This allows ignoring path separators throughout the import sequence, which could otherwise cause trouble depending on OS.
Resolve-Path can only be used for paths that already exist, Resolve-PSFPath can accept that the last leaf may not exist.
This is important when testing for paths.
#>


# Detect whether at some level loading individual module files, rather than the compiled module was enforced
$importIndividualFiles = Get-PSFConfigValue -FullName Monitoring.Import.IndividualFiles -Fallback $false
if ($Monitoring_importIndividualFiles) { $importIndividualFiles = $true }
if (Test-Path (Resolve-PSFPath -Path "$($script:ModuleRoot)\..\.git" -SingleItem -NewChild)) { $importIndividualFiles = $true }
if ("<was compiled>" -eq '<was not compiled>') { $importIndividualFiles = $true }
    
function Import-ModuleFile
{
    <#
        .SYNOPSIS
            Loads files into the module on module import.
         
        .DESCRIPTION
            This helper function is used during module initialization.
            It should always be dotsourced itself, in order to proper function.
             
            This provides a central location to react to files being imported, if later desired
         
        .PARAMETER Path
            The path to the file to load
         
        .EXAMPLE
            PS C:\> . Import-ModuleFile -File $function.FullName
     
            Imports the file stored in $function according to import policy
    #>

    [CmdletBinding()]
    Param (
        [string]
        $Path
    )
    
    $correctPath = $Path -replace '\\/',([IO.Path]::DirectorySeparatorChar)
    if ($doDotSource) { . $correctPath }
    else { $ExecutionContext.InvokeCommand.InvokeScript($false, ([scriptblock]::Create([io.file]::ReadAllText($correctPath))), $null, $null) }
}

#region Load individual files
if ($importIndividualFiles)
{
    # Execute Preimport actions
    . Import-ModuleFile -Path "$ModuleRoot\internal\scripts\preimport.ps1"
    
    # Import all internal functions
    foreach ($function in (Get-ChildItem "$ModuleRoot\internal\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore))
    {
        . Import-ModuleFile -Path $function.FullName
    }
    
    # Import all public functions
    foreach ($function in (Get-ChildItem "$ModuleRoot\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore))
    {
        . Import-ModuleFile -Path $function.FullName
    }
    
    # Execute Postimport actions
    . Import-ModuleFile -Path "$ModuleRoot\internal\scripts\postimport.ps1"
    
    # End it here, do not load compiled code below
    return
}
#endregion Load individual files

#region Load compiled code
<#
This file loads the strings documents from the respective language folders.
This allows localizing messages and errors.
Load psd1 language files for each language you wish to support.
Partial translations are acceptable - when missing a current language message,
it will fallback to English or another available language.
#>

Import-PSFLocalizedString -Path "$($script:ModuleRoot)\en-us\*.psd1" -Module 'Monitoring' -Language 'en-US'

function Add-Workload
{
<#
    .SYNOPSIS
        Start a worker agent for gathering data from monitored targets.
     
    .DESCRIPTION
        Creates a new runspaces and adds it to the worker agent pool.
        Then adds a tracking item for tracking results with Receive-Workload.
     
    .PARAMETER WorkloadPackage
        A workload package, containing target and the related checks.
     
    .EXAMPLE
        PS C:\> Add-Workload -WorkloadPackage $workload
     
        Start a worker agent for gathering data from the monitored target specified in the workload package.
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        $WorkloadPackage
    )
    
    begin
    {
        #region Main Invocation Scriptblock
        $scriptBlock = {
            param (
                $WorkLoad
            )
            
            $result = [pscustomobject]@{
                PSTypeName = 'Monitoring.CheckResult'
                Success    = $true
                Target       = $WorkLoad.Target
                Checks       = $WorkLoad.Checks
                Connected  = $false
                Results    = @{ }
                Errors       = @()
                ErrorChecks = @()
            }
            
            #region Establish Connections
            try
            {
                $connections = Connect-MonTarget -Name $WorkLoad.Target.Name -ErrorAction Stop
                $result.Connected = $true
            }
            catch
            {
                $result.Errors += $_
                $result.Success = $false
                return $result
            }
            #endregion Establish Connections
            
            #region Execute Scans
            foreach ($check in $WorkLoad.Checks)
            {
                try
                {
                    $result.Results[$check.Name] = @{
                        Timestamp = (Get-Date)
                        Result    = ($check.Check.Invoke($WorkLoad.Target.Name, $connections) | Write-Output)
                        Message   = ''
                    }
                }
                catch
                {
                    $result.Results[$check.Name] = @{
                        Timestamp = (Get-Date)
                        Result    = $null
                        Message   = $_.Exception.Message
                    }
                    $result.Errors += $_
                    $result.ErrorChecks += $check
                    $result.Success = $false
                }
            }
            #endregion Execute Scans
            
            #region Disconnect
            foreach ($capability in $WorkLoad.Target.Capability)
            {
                Disconnect-MonTarget -Capability $capability -Connection $connections -TargetName $WorkLoad.Target.Name -ErrorAction SilentlyContinue
            }
            #endregion Disconnect
            
            $result
        }
        #endregion Main Invocation Scriptblock
    }
    process
    {
        $powershell = [PowerShell]::Create().AddScript($scriptBlock).AddArgument($WorkloadPackage)
        $powershell.RunspacePool = $script:runspacePool
        $script:runspaces += [pscustomobject]@{
            Workload = $WorkloadPackage
            Runspace = $powerShell.BeginInvoke()
            PowerShell = $powerShell
            StartTime = (Get-Date)
            Received = $false
        }
    }
}


function ConvertFrom-Base64
{
<#
    .SYNOPSIS
        Converts a base64 encoded string back into its original form.
     
    .DESCRIPTION
        Converts a base64 encoded string back into its original form.
     
    .PARAMETER Text
        The base64 encoded string to convert
     
    .EXAMPLE
        PS C:\> 'RXhhbXBsZQ==' | ConvertFrom-Base64
     
        Converts the encoded 'RXhhbXBsZQ==' string into its human readable form ('Example')
#>

    [OutputType([System.String])]
    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline = $true, Mandatory = $true)]
        [string[]]
        $Text
    )
    
    process
    {
        foreach ($entry in $Text)
        {
            [Text.Encoding]::UTF8.GetString(([System.Convert]::FromBase64String($entry)))
        }
    }
}

function ConvertTo-Base64
{
<#
    .SYNOPSIS
        Converts text to base 64 encoded string.
     
    .DESCRIPTION
        Converts text to base 64 encoded string.
     
    .PARAMETER Text
        The text to convert.
     
    .EXAMPLE
        PS C:\> 'Example' | ConvertTo-Base64
     
        Converts the string 'Example' into its base64 representation.
#>

    [OutputType([System.String])]
    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline = $true, Mandatory = $true)]
        [string[]]
        $Text
    )
    
    process
    {
        foreach ($entry in $Text)
        {
            [System.Convert]::ToBase64String(([Text.Encoding]::UTF8.GetBytes($entry)))
        }
    }
}

function Export-Config
{
<#
    .SYNOPSIS
        Persists configuration data controlling the module's execution.
     
    .DESCRIPTION
        Persists configuration data controlling the module's execution.
     
    .PARAMETER Type
        What kind of configuration data to export:
        - All : Everything (default)
        - Target : Configuration information about targets to service
        - Limit : The limits to use for determining alert states
     
    .PARAMETER TargetName
        Filter what targets should be affected.
        By default, all targets are exported.
     
    .EXAMPLE
        PS C:\> Export-Config
     
        Export all configuration data regarding all targets and limits.
#>

    [CmdletBinding()]
    Param (
        [ValidateSet('All', 'Target', 'Limit')]
        [string]
        $Type = 'All',
        
        [string]
        $TargetName = '*'
    )
    
    begin
    {
        $activeSource = Get-PSFConfigValue -FullName 'Monitoring.Source.Config.Active'
        $sourceItem = $script:configSources[$activeSource]
        if (-not $sourceItem)
        {
            Stop-PSFFunction -String 'Import-Config.SourceNotFound' -StringValues $activeSource -EnableException $true -Cmdlet $PSCmdlet
        }
    }
    process
    {
        $params = $Type, $TargetName
        $ExecutionContext.InvokeCommand.InvokeScript($true, $sourceItem.ExportScript, $null, $params)
    }
}


function Export-Data
{
<#
    .SYNOPSIS
        Export the gathered data to cache.
     
    .DESCRIPTION
        Export the gathered data to cache.
     
    .PARAMETER TargetName
        Target name filter of what to export.
     
    .EXAMPLE
        PS C:\> Export-Data
     
        Export all gathered data to cache.
#>

    [CmdletBinding()]
    param (
        [string]
        $TargetName = '*'
    )
    
    begin
    {
        $activeSource = Get-PSFConfigValue -FullName 'Monitoring.Source.Data.Active'
        $sourceItem = $script:dataSources[$activeSource]
        if (-not $sourceItem)
        {
            Stop-PSFFunction -String 'Import-Data.SourceNotFound' -StringValues $activeSource -EnableException $true -Cmdlet $PSCmdlet
        }
    }
    process
    {
        $ExecutionContext.InvokeCommand.InvokeScript($true, $sourceItem.ExportScript, $null, $TargetName)
    }
}


function Import-Config
{
<#
    .SYNOPSIS
        Imports configuration data from cache.
     
    .DESCRIPTION
        Imports configuration data from cache.
     
    .PARAMETER Type
        What kind of configuration data to import:
        - All : Everything (default)
        - Target : Configuration information about targets to service
        - Limit : The limits to use for determining alert states
     
    .PARAMETER TargetName
        Filter what targets should be affected.
        By default, all targets are imported.
     
    .EXAMPLE
        PS C:\> Import-Config
     
        Imports all configuration data
#>

    [CmdletBinding()]
    param (
        [ValidateSet('All', 'Target', 'Limit')]
        [string]
        $Type = 'All',
        
        [string]
        $TargetName = '*'
    )
    
    begin
    {
        $activeSource = Get-PSFConfigValue -FullName 'Monitoring.Source.Config.Active'
        $sourceItem = $script:configSources[$activeSource]
        if (-not $sourceItem)
        {
            Stop-PSFFunction -String 'Import-Config.SourceNotFound' -StringValues $activeSource -EnableException $true -Cmdlet $PSCmdlet
        }
    }
    process
    {
        $params = $Type, $TargetName
        $ExecutionContext.InvokeCommand.InvokeScript($true, $sourceItem.ImportScript, $null, $params)
    }
}


function Import-Data
{
<#
    .SYNOPSIS
        Gathers target data from disk.
     
    .DESCRIPTION
        Gathers target data from disk.
     
    .PARAMETER TargetName
        The target for which to retrieve data.
        Defaults to all items.
        target data is stored on disk under base64-encoded filename for compatibility reasons.
     
    .EXAMPLE
        PS C:\> Import-Data
     
        Imports all cached data.
#>

    [CmdletBinding()]
    param (
        [string]
        $TargetName = '*'
    )
    
    begin
    {
        $activeSource = Get-PSFConfigValue -FullName 'Monitoring.Source.Data.Active'
        $sourceItem = $script:dataSources[$activeSource]
        if (-not $sourceItem)
        {
            Stop-PSFFunction -String 'Import-Data.SourceNotFound' -StringValues $activeSource -EnableException $true -Cmdlet $PSCmdlet
        }
    }
    process
    {
        $ExecutionContext.InvokeCommand.InvokeScript($true, $sourceItem.ImportScript, $null, $TargetName)
    }
}


function Receive-Workload
{
<#
    .SYNOPSIS
        Waits for worker agents to finish and receives their results.
     
    .DESCRIPTION
        Waits for worker agents to finish and receives their results.
        Returns all result objects generated by each worker.
        Use of this commands assumes that:
        - Start-WorkloadManager was used to prepare worker agent execution
        - Add-Workload was used to queue data gathering agents for the affected targets.
     
    .EXAMPLE
        PS C:\> Receive-Workload
     
        Waits for worker agents to finish and receives their results.
     
    .NOTES
        Results are also stored in the module-scope $script:lastCheckResults variable.
        This is strictly for debugging purposes
#>

    [CmdletBinding()]
    param (
        
    )
    
    process
    {
        while ($script:runspaces | Where-Object Received -EQ $false)
        {
            #region Collect Data from finished workers
            foreach ($runspaceContainer in ($script:runspaces | Where-Object { -not $_.Received -and $_.Runspace.IsCompleted }))
            {
                $resultObject = $runspaceContainer.PowerShell.EndInvoke($runspaceContainer.Runspace)
                $runspaceContainer.PowerShell.Dispose()
                $script:lastCheckResults += $resultObject
                
                if (-not $resultObject.Connected)
                {
                    $runspaceContainer.Received = $true
                    Write-Error "Failed to connect to $($resultObject.Target.Name) : $($resultObject.Errors.Exception.Message)" -TargetObject $runspaceContainer
                    continue
                }
                
                Import-Data -TargetName $resultObject.Target.Name
                if (-not $script:data[$resultObject.Target.Name])
                {
                    $script:data[$resultObject.Target.Name] = @{ }
                }
                foreach ($key in $resultObject.Results.Keys)
                {
                    if ($resultObject.ErrorChecks.Name -contains $key)
                    {
                        Write-Warning "Failed to check $key on $($resultObject.Target.Name) : $($resultObject.Results[$key].Message)"
                        continue
                    }
                    $script:data[$resultObject.Target.Name][$key] = $resultObject.Results[$key]
                }
                Export-Data -TargetName $resultObject.Target.Name
                
                $runspaceContainer.Received = $true
                $resultObject
            }
            #endregion Collect Data from finished workers
            
            #region Terminate worker agents that timed out
            foreach ($runspaceContainer in ($script:runspaces | Where-Object { -not $_.Received -and ($_.StartTime.Add((Get-PSFConfigValue -FullName 'Monitoring.Runspace.ExecutionTimeout')) -lt (Get-Date)) }))
            {
                Write-Error "Timeout: Gathering data from $($runspaceContainer.Workload.Target.Name)" -TargetObject $runspaceContainer
                $runspaceContainer.PowerShell.Dispose()
                $runspaceContainer.Received = $true
            }
            #endregion Terminate worker agents that timed out
            
            # Prevent max CPU load while runspaces are still busy
            Start-Sleep -Milliseconds 100
        }
    }
}


function Start-WorkloadManager
{
<#
    .SYNOPSIS
        Generates a clean runspace pool for operating data gathering workloads.
     
    .DESCRIPTION
        Generates a clean runspace pool for operating data gathering workloads.
     
    .EXAMPLE
        PS C:\> Start-WorkloadManager
     
        Generates a clean runspace pool for operating data gathering workloads.
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding()]
    Param (
    
    )
    
    begin
    {
        # Dispose of old runspace pools in case execution was interrupted
        if ($script:runspacePool -and -not $script:runspacePool.IsDisposed) { $script:runspacePool.Dispose() }
    }
    process
    {
        # Create a runspace pool with the same instance of the module imported
        $initialSessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault()
        $null = $initialSessionState.ImportPSModule("$($script:ModuleRoot)\Monitoring.psd1")
        $script:runspacePool = [RunspaceFactory]::CreateRunspacePool($initialSessionState)
        $null = $script:runspacePool.SetMinRunspaces(1)
        $null = $script:runspacePool.SetMaxRunspaces((Get-PSFConfigValue -FullName 'Monitoring.Runspace.MaxWorkerCount'))
        $script:runspacePool.Open()
        
        # Declare runtime variable storing the worker agent information
        $script:runspaces = @()
        
        # In-Memory Result Cache. For debugging purposes only.
        $script:lastCheckResults = @()
    }
}


function Stop-WorkloadManager
{
<#
    .SYNOPSIS
        Cleans up all leftovers from the worker agents.
     
    .DESCRIPTION
        Cleans up all leftovers from the worker agents.
     
    .EXAMPLE
        PS C:\> Stop-WorkloadManager
     
        Cleans up all leftovers from the worker agents.
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding()]
    Param (
    
    )
    
    process
    {
        if ($script:runspacePool -and -not $script:runspacePool.IsDisposed)
        {
            $script:runspacePool.Dispose()
        }
    }
}


function Test-Overlap
{
<#
    .SYNOPSIS
        Matches N:N mappings for congruence.
     
    .DESCRIPTION
        Matches N:N mappings for congruence.
        Use this for comparing two arrays for overlap.
        This can be used for scenarios such as:
        - Whether n Items in Array One are equal to an Item in Array Two.
        - Whether n Items in Array One are similar to an Item in Array Two.
        This is especially designed to abstract filtering by multiple wildcard filters.
     
    .PARAMETER ReferenceObject
        The object(s) to compare
     
    .PARAMETER DifferenceObject
        The array of items to compare them to.
     
    .PARAMETER Property
        Compare a property, rather than the basic object.
     
    .PARAMETER Count
        The number of congruent items required for a successful result.
        Defaults to 1.
     
    .PARAMETER Operator
        How the comparison should be performed.
        Defaults to 'Equal'
        Supported Comparisons: Equal, Like, Match
     
    .EXAMPLE
        PS C:\> Test-Overlap -ReferenceObject $ReferenceObject -DifferenceObject $DifferenceObject
     
        Tests whether any item in the two arrays are equal.
#>

    [OutputType([System.Boolean])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [AllowNull()]
        $ReferenceObject,
        
        [Parameter(Mandatory = $true)]
        [AllowNull()]
        $DifferenceObject,
        
        [string]
        $Property,
        
        [int]
        $Count = 1,
        
        [ValidateSet('Equal', 'Like', 'Match')]
        [string]
        $Operator = 'Equal'
    )
    
    begin
    {
        $parameter = @{
            IncludeEqual = $true
            ExcludeDifferent = $true
        }
        if ($Property) { $parameter['Property'] = $Property }
    }
    process
    {
        switch ($Operator)
        {
            'Equal'
            {
                return (Compare-Object -ReferenceObject $ReferenceObject -DifferenceObject $DebugPreference @parameter | Measure-Object).Count -ge $Count
            }
            'Like'
            {
                $numberFound = 0
                foreach ($reference in $ReferenceObject)
                {
                    foreach ($difference in $DifferenceObject)
                    {
                        if ($Property -and ($reference.$Property -like $difference.$Property)) { $numberFound++ }
                        elseif (-not $Property -and ($reference -like $difference)) { $numberFound++ }
                        
                        if ($numberFound -ge $Count) { return $true }
                    }
                }
                
                return $false
            }
            'Match'
            {
                $numberFound = 0
                foreach ($reference in $ReferenceObject)
                {
                    foreach ($difference in $DifferenceObject)
                    {
                        if ($Property -and ($reference.$Property -match $difference.$Property)) { $numberFound++ }
                        elseif (-not $Property -and ($reference -match $difference)) { $numberFound++ }
                        
                        if ($numberFound -ge $Count) { return $true }
                    }
                }
                
                return $false
            }
        }
    }
}


function Get-MonCheck
{
<#
    .SYNOPSIS
        Returns registered checks.
     
    .DESCRIPTION
        Returns registered checks.
     
    .PARAMETER Tag
        The tag to filter by.
     
    .PARAMETER Name
        The name to filter by
     
    .EXAMPLE
        PS C:\> Get-MonCheck
     
        Returns all registered checks.
#>

    [CmdletBinding()]
    Param (
        [string[]]
        $Tag = '*',
        
        [string[]]
        $Name = '*'
    )
    
    process
    {
        $checks = foreach ($checkItem in $script:checks.Values)
        {
            #region Filter by Name
            $foundName = $false
            foreach ($nameItem in $Name)
            {
                if ($checkItem.Name -like $nameItem)
                {
                    $foundName = $true
                    break
                }
            }
            if (-not $foundName) { continue }
            #endregion Filter by Name
            
            #region Filter by Tag
            $foundTag = $false
            foreach ($tagItem in $Tag)
            {
                if ($checkItem.Tag -like $tagItem)
                {
                    $foundTag = $true
                    break
                }
            }
            if (-not $foundTag) { continue }
            #endregion Filter by Tag
            
            $clonedCheck = $checkItem.Clone()
            $clonedCheck['PSTypeName'] = 'Monitoring.Check'
            
            [PSCustomObject]$clonedCheck
        }
        $checks | Sort-Object Name
    }
}


function Invoke-MonCheck
{
<#
    .SYNOPSIS
        Command that gathers data as configured.
     
    .DESCRIPTION
        The main data gathering command.
        Schedule this command as a scheduled task after setting up the targets, connection capabilities and checks.
     
    .PARAMETER Tag
        The tags to scan for.
     
    .PARAMETER TargetName
        The targets to scan.
     
    .PARAMETER Name
        The name of the checks to execute.
     
    .EXAMPLE
        PS C:\> Invoke-MonCheck
     
        Executes all checks.
#>

    [CmdletBinding()]
    param (
        [string[]]
        $Tag = '*',
        
        [string[]]
        $TargetName = '*',
        
        [string[]]
        $Name = '*'
    )
    
    begin
    {
        Import-Config
        
        #region Auto Import Management Packs
        if (-not $script:triedAutoImport)
        {
            $script:triedAutoImport = $true
            
            #region Import Registered Management Pack Modules
            foreach ($moduleName in (Get-PSFConfigValue -FullName 'Monitoring.ManagementPack.Import'))
            {
                try
                {
                    Write-PSFMessage -String 'Import.ManagementPack.Import' -StringValues $moduleName -ModuleName Monitoring
                    Import-Module -Name $moduleName -Scope Global -ErrorAction Stop
                }
                catch
                {
                    Write-PSFMessage -Level Warning -String 'Import.ManagementPack.Import.Failed' -StringValues $moduleName -ModuleName Monitoring
                }
            }
            #endregion Import Registered Management Pack Modules
            
            #region Import all detected Management Packs if enabled
            if (Get-PSFConfigValue -FullName 'Monitoring.ManagementPack.AutoLoad')
            {
                $psd1Files = Get-Item "C:\Program Files\WindowsPowerShell\Modules\*\*\*.psd1"
                $allManagementPackModules = foreach ($psd1File in $psd1Files)
                {
                    $data = Import-PowerShellDataFile -Path $psd1File.FullName -ErrorAction Ignore
                    if (-not $data) { continue }
                    if (-not ($data.PrivateData.PSData.Tags -eq 'MonitoringManagementPack')) { continue }
                    $data['FileName'] = $psd1File.BaseName
                    $data['Path'] = $psd1File.FullName
                    $data['ModuleVersion'] = [version]$data['ModuleVersion']
                    [pscustomobject]$data
                }
                $toImport = $allManagementPackModules | Group-Object FileName | ForEach-Object {
                    $_.Group | Sort-Object ModuleVersion -Descending | Select-Object -First 1 -ExpandProperty Path
                }
                foreach ($moduleManifest in $toImport)
                {
                    try
                    {
                        Write-PSFMessage -String 'Import.ManagementPack.Import' -StringValues $moduleManifest -ModuleName Monitoring
                        Import-Module $moduleManifest -Scope Global -ErrorAction Stop
                    }
                    catch
                    {
                        Write-PSFMessage -Level Warning -String 'Import.ManagementPack.Import.Failed' -StringValues $moduleManifest -ModuleName Monitoring
                    }
                }
            }
            #endregion Import all detected Management Packs if enabled
        }
        #endregion Auto Import Management Packs
        
        Start-WorkloadManager
    }
    process
    {
        $checks = Get-MonCheck -Tag $Tag -Name $Name
        $targets = Get-MonTarget -Name $TargetName -Tag $Tag
        
        foreach ($targetItem in $targets)
        {
            $workload = [pscustomobject]@{
                PSTypeName = 'Monitoring.Workload'
                Target       = $targetItem
                Checks       = ($checks | Where-Object { Test-Overlap -ReferenceObject $targetItem -DifferenceObject $_ -Property Tag -Operator Like })
            }
            if (-not $workload.Checks) { continue }
            
            Add-Workload -WorkloadPackage $workload
        }
        
        Receive-Workload
    }
    end
    {
        Stop-WorkloadManager
    }
}


function Register-MonCheck
{
<#
    .SYNOPSIS
        Register the logic used to scan a target for a given point of information.
     
    .DESCRIPTION
        Register a scriptblock that will be used to gather a piece of information from the target.
        This scriptblock will receive two arguments:
        - The name of the target
        - A hashtable of connections (as registered using Register-MonConnection and applied to the target by its Capability property)
     
    .PARAMETER Name
        The name of the check to register.
        Must be unique or it will overwrite the other check.
     
    .PARAMETER Check
        The logic to execute.
        This scriptblock will receive two arguments:
        - The name of the target
        - A hashtable of connections (as registered using Register-MonConnection and applied to the target by its Capability property)
     
    .PARAMETER Tag
        The tags this check applies to.
        Tags are arbitrary labels that group monitoring targets.
        A target has one or more tags, and all checks with a matching tag are applied to the target.
     
    .PARAMETER Description
        Adds a description to the check, explaining what this check does and requires.
     
    .PARAMETER Module
        The Management Pack Module that introduced the check.
     
    .PARAMETER RecommendedLimit
        Adds a recommended limit to the check.
        This is intended to offer the opportunity to give a sane default setting.
     
        Caveat:
        Under no circumstances is this an assumption that this limit is a good fit for every environment.
        Consider this a starting point if you are unsure, what your actual limits should be like.
     
    .PARAMETER RecommendedLimitOperator
        The operator to apply to the recommended limit.
        See the caveat on the parameter help for RecommendedLimit.
     
    .EXAMPLE
        PS C:\> Register-MonCheck -Name 'NTDS_DB_FreeDiskPercent' -Check $checkScript -Tag 'dc' -Description 'Returns the percent of free space on the disk hosting the NTDS Database.'
         
        Registers the logic stored in the $checkScript variable under the name 'NTDS_DB_FreeDiskPercent' and assigns it to the 'dc' label.
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string]
        $Name,
        
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [System.Management.Automation.ScriptBlock]
        $Check,
        
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string[]]
        $Tag,
        
        [string]
        $Description,
        
        [string]
        $Module,
        
        [object]
        $RecommendedLimit,
        
        [Monitoring.LimitOperator]
        $RecommendedLimitOperator = 'LessThan'
    )
    
    process
    {
        $script:checks[$Name] = @{
            Name                     = $Name
            Tag                         = $Tag
            Check                     = $Check
            Description                 = $Description
            Module                     = $Module
            RecommendedLimit         = $RecommendedLimit
            RecommendedLimitOperator = $RecommendedLimitOperator
        }
    }
}

function Connect-MonTarget
{
<#
    .SYNOPSIS
        Establish a connection to the specified target.
     
    .DESCRIPTION
        Establish a connection to the specified target.
        All configured capabilities will be considered.
     
    .PARAMETER Name
        The name of the target to connect to.
     
    .EXAMPLE
        PS C:\> Connect-MonTarget -Name server.contoso.com
     
        Establishes a connection to server.contoso.com
#>

    [OutputType([Hashtable])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )
    
    begin
    {
        $target = Get-MonTarget -Name $Name
    }
    process
    {
        [hashtable]$connections = @{ }
        foreach ($capability in $target.Capability)
        {
            if (-not $script:connectionTypes.ContainsKey($capability))
            {
                Write-Error "Connection Type: $capability not found!"
                continue
            }
            
            try
            {
                $tempResult = $script:connectionTypes[$capability].Connect.Invoke($Name) | Select-Object -First 1
                foreach ($key in $tempResult.Keys) { $connections[$key] = $tempResult[$key] }
            }
            catch
            {
                Write-Error "Failed to connect to $Name via $capability : $_"
                continue
            }
        }
        $connections
    }
}


function Disconnect-MonTarget
{
<#
    .SYNOPSIS
        Disconnects all sessions for a given target.
     
    .DESCRIPTION
        Disconnects all sessions for a given target.
     
    .PARAMETER Capability
        The capability for which to cancel the connection.
     
    .PARAMETER Connection
        The hashtable of connections generated from Connect-MonTarget
     
    .PARAMETER TargetName
        The target to disconnect from.
     
    .EXAMPLE
        PS C:\> Disconnect-MonTarget -Capability 'WinRM' -Connection $Connection -TargetName server.contoso.com
     
        Disconnects all WinRM related sessions for server.contoso.com
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $Capability,
        
        [Parameter(Mandatory = $true)]
        $Connection,
        
        [string]
        $TargetName
    )
    
    process
    {
        if (-not $script:connectionTypes.ContainsKey($Capability))
        {
            Write-Error "Connection Type: $Capability not found!"
            return
        }
        
        try { $script:connectionTypes[$Capability].Disconnect.Invoke($Connection, $TargetName) }
        catch
        {
            Write-Error "Failed to disconnect from $Capability : $_"
            return
        }
    }
}

function Get-MonConnection
{
<#
    .SYNOPSIS
        Returns registered connections based on capability.
     
    .DESCRIPTION
        Returns registered connections based on capability.
     
    .PARAMETER Capability
        The capability for which to look for connections.
     
    .EXAMPLE
        PS C:\> Get-MonConnection -Capability WinRM
     
        Returns the registered connection logic for connecting via WinRM
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string[]]
        $Capability
    )
    
    process
    {
        foreach ($capabilityItem in $Capability)
        {
            if (-not $script:connectionTypes[$capabilityItem])
            {
                Write-Error "Connection Capability $capabilityItem has no matching connection script!"
                continue
            }
            
            [pscustomobject]@{
                PSTypeName = 'Monitoring.Connection'
                Name       = $capabilityItem
                Connection = $script:connectionTypes[$capabilityItem]
            }
        }
    }
}


function Register-MonConnection
{
<#
    .SYNOPSIS
        Registers logic that connects to targets.
     
    .DESCRIPTION
        Registers logic that connects to targets.
        Use this to add capabilities to the module, that can then be used to connect to a target and be leveraged by checks.
     
    .PARAMETER Capability
        The name to assign to the capability.
     
    .PARAMETER ConnectionScript
        The script to connect to a target.
        Only receives the name of the target as argument.
        Must return a hashtable, either with a unique name and the connection object, or an empty hashtable.
        The hashtable may contain more than one entry and will be merged with other entires, if a target supports multiple capabilities.
     
    .PARAMETER DisconnectionScript
        The script to disconnect from a target.
        Receives two arguments:
        - A hashtable of connections
        - The name of the target
        The hastable in question contains ALL connections from all capabilities applicable to the target.
     
    .EXAMPLE
        PS C:\> Register-MonConnection -Capability 'WinRM' -ConnectionScript $connect -DisconnectionScript $disconnect
     
        Registers the WinRM capability with logic to connect and logic to disconnect.
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $Capability,
        
        [Parameter(Mandatory = $true)]
        [System.Management.Automation.ScriptBlock]
        $ConnectionScript,
        
        [System.Management.Automation.ScriptBlock]
        $DisconnectionScript = { }
    )
    
    process
    {
        $script:connectionTypes[$Capability] = @{
            Name = $Capability
            Connect = $ConnectionScript
            Disconnect = $DisconnectionScript
        }
    }
}


function Get-MonLimit
{
<#
    .SYNOPSIS
        Returns registered check limits.
     
    .DESCRIPTION
        Returns registered check limits.
     
    .PARAMETER TargetName
        The name of the target for which to check limits.
     
    .PARAMETER CheckName
        The name of the check to look for.
     
    .EXAMPLE
        PS C:\> Get-MonLimit
     
        Returns all limits registered.
#>

    [CmdletBinding()]
    Param (
        [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('Name')]
        [string[]]
        $TargetName = "*",
        
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [string[]]
        $CheckName = '*'
    )
    
    begin
    {
        Import-Config
    }
    process
    {
        foreach ($targetItem in (Get-MonTarget -Name $TargetName))
        {
            $script:Limits.$($targetItem.Name).Values | Where-Object { Test-Overlap -ReferenceObject $_.CheckName -DifferenceObject $CheckName -Operator Like } | ForEach-Object {
                $clonedTable = $_.Clone()
                $clonedTable['PSTypeName'] = 'Monitoring.Limit'
                [pscustomobject]$clonedTable
            }
        }
    }
}


function Set-MonLimit
{
<#
    .SYNOPSIS
        Applies a limit/threshold about what constitutes a warning/error.
     
    .DESCRIPTION
        Applies a limit/threshold about what constitutes a warning/error.
     
    .PARAMETER TargetName
        The name of the target to apply it to.
        The targets must already exist for this to be considered.
        By default, ALL targets are considered.
     
    .PARAMETER CheckName
        The check for which to apply a limit.
        The check does not have to exist before applying a limit.
     
    .PARAMETER ErrorLimit
        The threshold that needs to be crossed for the state to be considered in Error.
     
    .PARAMETER WarningLimit
        The threshold that needs to be crossed for the state to be considered in Warning.
     
    .PARAMETER Operator
        What operator to apply to the limit.
        For example, setting the Operator to 'GreaterThan' and the ErrorLimit to 80 would have all results greater than 80 be considered in error.
     
    .EXAMPLE
        PS C:\> Get-MonTarget -Tag DC | Set-MonLimit -CheckName 'LogDriveFreeSpacePercent' -ErrorLimit 10 -WarningLimit 20 -Operator LessThan
     
        Updates all targets of the type DC to new limit thresholds for the check LogDriveFreeSpacePercent:
        - Warning as soon as the result sinks below '20'
        - Error as soon as the result sinks below '10'
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('Name')]
        [string[]]
        $TargetName = "*",
        
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string]
        $CheckName,
        
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [object]
        $ErrorLimit,
        
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [object]
        $WarningLimit,
        
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [Monitoring.LimitOperator]
        [string]
        $Operator
    )
    
    process
    {
        foreach ($targetItem in (Get-MonTarget -Name $TargetName))
        {
            Import-Config -TargetName $targetItem.Name -Type Limit
            if (-not $script:limits[$targetItem.Name]) { $script:limits[$targetItem.Name] = @{ } }
            $script:limits[$targetItem.Name][$CheckName] = @{
                TargetName   = $targetItem.Name
                CheckName    = $CheckName
                ErrorLimit   = $ErrorLimit
                WarningLimit = $WarningLimit
                Operator     = $Operator
            }
            Export-Config -TargetName $targetItem.Name -Type Limit
        }
    }
}


function Test-MonHealth
{
<#
    .SYNOPSIS
        Returns cached data and compares it with configured alert limits (if present).
     
    .DESCRIPTION
        Returns cached data and compares it with configured alert limits (if present).
     
    .PARAMETER TargetName
        Filter by target.
     
    .PARAMETER Tag
        Filter by assigned tag to that target.
     
    .PARAMETER CheckName
        Filter by applied check.
     
    .EXAMPLE
        PS C:\> Test-MonHealth
     
        Returns all scanend data for all targets and all checks.
#>

    [CmdletBinding()]
    param (
        [string[]]
        $TargetName = '*',
        
        [string[]]
        $Tag = '*',
        
        [string[]]
        $CheckName = '*'
    )
    
    begin
    {
        #region Utility Function
        function Add-Result
        {
            [CmdletBinding()]
            param (
                [string]
                $Name,
                
                $Data,
                
                [hashtable]
                $Result,
                
                [string]
                $CheckName,
                
                [string]
                $TargetName,
                
                $WarningLimit,
                
                $ErrorLimit,
                
                [string]
                $Operator,
                
                [switch]
                $Finalize
            )
            
            #region Finalize and return objects
            if ($Finalize)
            {
                foreach ($resultItem in $Result.Values)
                {
                    #region Case: No data gathered yet
                    if (-not $resultItem.Timestamp)
                    {
                        $resultItem.Status = 'Error'
                        $resultItem
                        continue
                    }
                    #endregion Case: No data gathered yet
                    
                    #region Case: Stale Data
                    if ($resultItem.Timestamp.Add((Get-PSFConfigValue -FullName 'Monitoring.Data.StaleTimeout')) -lt (Get-Date))
                    {
                        $resultItem.Status = 'Error'
                        $resultItem
                        continue
                    }
                    #endregion Case: Stale Data
                    
                    #region Case: Valid Data
                    switch ($resultItem.Operator)
                    {
                        'GreaterThan'
                        {
                            if ($resultItem.Value -gt $resultItem.WarningLimit) { $resultItem.Status = 'Warning' }
                            if ($resultItem.Value -gt $resultItem.AlarmLimit) { $resultItem.Status = 'Error' }
                            break
                        }
                        'GreaterEqual'
                        {
                            if ($resultItem.Value -ge $resultItem.WarningLimit) { $resultItem.Status = 'Warning' }
                            if ($resultItem.Value -ge $resultItem.AlarmLimit) { $resultItem.Status = 'Error' }
                            break
                        }
                        'Equal'
                        {
                            if ($resultItem.Value -eq $resultItem.WarningLimit) { $resultItem.Status = 'Warning' }
                            if ($resultItem.Value -eq $resultItem.AlarmLimit) { $resultItem.Status = 'Error' }
                            break
                        }
                        'NotEqual'
                        {
                            if ($resultItem.Value -ne $resultItem.WarningLimit) { $resultItem.Status = 'Warning' }
                            if ($resultItem.Value -ne $resultItem.AlarmLimit) { $resultItem.Status = 'Error' }
                            break
                        }
                        'LessEqual'
                        {
                            if ($resultItem.Value -le $resultItem.WarningLimit) { $resultItem.Status = 'Warning' }
                            if ($resultItem.Value -le $resultItem.AlarmLimit) { $resultItem.Status = 'Error' }
                            break
                        }
                        'LessThan'
                        {
                            if ($resultItem.Value -lt $resultItem.WarningLimit) { $resultItem.Status = 'Warning' }
                            if ($resultItem.Value -lt $resultItem.AlarmLimit) { $resultItem.Status = 'Error' }
                            break
                        }
                        'Like'
                        {
                            if ($resultItem.Value -like $resultItem.WarningLimit) { $resultItem.Status = 'Warning' }
                            if ($resultItem.Value -like $resultItem.AlarmLimit) { $resultItem.Status = 'Error' }
                            break
                        }
                        'NotLike'
                        {
                            if ($resultItem.Value -notlike $resultItem.WarningLimit) { $resultItem.Status = 'Warning' }
                            if ($resultItem.Value -notlike $resultItem.AlarmLimit) { $resultItem.Status = 'Error' }
                            break
                        }
                        'Match'
                        {
                            if ($resultItem.Value -match $resultItem.WarningLimit) { $resultItem.Status = 'Warning' }
                            if ($resultItem.Value -match $resultItem.AlarmLimit) { $resultItem.Status = 'Error' }
                            break
                        }
                        'NotMatch'
                        {
                            if ($resultItem.Value -notmatch $resultItem.WarningLimit) { $resultItem.Status = 'Warning' }
                            if ($resultItem.Value -notmatch $resultItem.AlarmLimit) { $resultItem.Status = 'Error' }
                            break
                        }
                        default
                        {
                            $resultItem.Status = 'No Limit'
                        }
                    }
                    $resultItem
                    #endregion Case: Valid Data
                }
            }
            #endregion Finalize and return objects
            
            if (-not $Result[$Name])
            {
                $Result[$Name] = [pscustomobject]@{
                    PSTypeName = 'Monitoring.HealthResult'
                    Target       = $TargetName
                    Check       = $CheckName
                    Value       = $Data.Result
                    Timestamp  = $Data.Timestamp
                    Message    = $Data.Message
                    WarningLimit = $WarningLimit
                    ErrorLimit = $ErrorLimit
                    Operator   = $Operator
                    Status       = 'Healthy'
                }
            }
            else
            {
                # Can only happen when processing limits that have matching data
                $Result[$Name].WarningLimit = $WarningLimit
                $Result[$Name].ErrorLimit = $ErrorLimit
                $Result[$Name].Operator = $Operator
            }
        }
        #endregion Utility Function
        
        Import-Config
    }
    process
    {
        foreach ($targetItem in (Get-MonTarget -Name $TargetName -Tag $Tag))
        {
            Import-Data -TargetName $targetItem
            $result = @{ }
            
            foreach ($key in $script:data[$targetItem.Name].Keys)
            {
                if (-not (Test-Overlap -ReferenceObject $key -DifferenceObject $CheckName -Operator Like)) { continue }
                Add-Result -Name $key -Data $script:data[$targetItem.Name][$key] -Result $result -CheckName $key -TargetName $targetItem.Name
            }
            
            foreach ($limitItem in $script:limits[$targetItem.Name].Values)
            {
                if (-not (Test-Overlap -ReferenceObject $limitItem.CheckName -DifferenceObject $CheckName -Operator Like)) { continue }
                $paramAddResult = @{
                    Name = $limitItem.CheckName
                    Result = $result
                    CheckName = $key
                    TargetName = $targetItem.Name
                    WarningLimit = $limitItem.WarningLimit
                    ErrorLimit = $limitItem.ErrorLimit
                    Operator = $limitItem.Operator
                }
                Add-Result @paramAddResult
            }
            Add-Result -Result $result -Finalize
        }
    }
}


function Get-MonDatum
{
<#
    .SYNOPSIS
        Returns information on a single piece of scanned data.
     
    .DESCRIPTION
        Returns information on a single piece of scanned data.
     
        Returns an object with three properties:
        - Timestamp (When was the data last retrieved)
        - Result (What data was retrieved)
        - Message (Any error message)
        Any content in Message implies a failed result.
        If no data was found for the specified combination of target and check, the message property will list "No Data".
     
    .PARAMETER TargetName
        The name of the target to retrive data for.
        No wildcards.
     
    .PARAMETER CheckName
        The name of the check to retrive data for.
        No wildcards.
     
    .EXAMPLE
        PS C:\> Get-MonDatum -TargetName sever.contoso.com -CheckName NTDS.DBDiskFreeSpacePercent
     
        Returns the check result of NTDS.DBDiskFreeSpacePercent for sever.contoso.com
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $TargetName,
        
        [Parameter(Mandatory = $true)]
        [string]
        $CheckName
    )
    
    begin
    {
        Import-Data -TargetName $TargetName
    }
    process
    {
        $datum = $script:data.$TargetName.$CheckName
        
        if ($datum)
        {
            $clonedDatum = $datum.Clone()
            $clonedDatum['PSTypeName'] = 'Monitoring.Datum'
            [pscustomobject]$clonedDatum
        }
        else
        {
            [pscustomobject]@{
                PSTypeName = 'Monitoring.Datum'
                Timestamp = $null
                Result    = $null
                Message   = 'No Data'
            }
        }
    }
}


function Get-MonConfigSource
{
<#
    .SYNOPSIS
        Returns the registered config sources.
     
    .DESCRIPTION
        Returns the registered config sources.
     
    .PARAMETER Name
        The name of the source to return.
     
    .EXAMPLE
        PS C:\> Get-MonConfigSource
     
        Lists all config sources.
#>

    [CmdletBinding()]
    param (
        [string]
        $Name = '*'
    )
    
    process
    {
        $script:configSources.Values | Where-Object Name -Like $Name | ForEach-Object {
            $clonedHashtable = $_.Clone()
            $clonedHashtable['PSTypeName'] = 'Monitoring.ConfigSource'
            [pscustomobject]$clonedHashtable
        }
    }
}

function Get-MonDataSource
{
<#
    .SYNOPSIS
        Returns the registered data sources.
     
    .DESCRIPTION
        Returns the registered data sources.
     
    .PARAMETER Name
        The name of the source to return.
     
    .EXAMPLE
        PS C:\> Get-MonDataSource
     
        Lists all data sources.
#>

    [CmdletBinding()]
    param (
        [string]
        $Name = '*'
    )
    
    process
    {
        $script:dataSources.Values | Where-Object Name -Like $Name | ForEach-Object {
            $clonedHashtable = $_.Clone()
            $clonedHashtable['PSTypeName'] = 'Monitoring.DataSource'
            [pscustomobject]$clonedHashtable
        }
    }
}

function Register-MonConfigSource
{
<#
    .SYNOPSIS
        Registers a custom monitoring configuration source.
     
    .DESCRIPTION
        Registers a custom monitoring configuration source.
        Config sources are the configuration backend that define the data gathering behavior.
        This includes the targets to monitor and any limits to apply in scenarios where the limit configuration is stored in the module configuration itself.
        For example, the 'Path' config source that comes with the module (and is the default source) will store the configuration in file.
        This command makes the actual configuration management freely extensible.
     
    .PARAMETER Name
        The name of the source. Must be unique, otherwise the previous config source will be overwritten,
     
    .PARAMETER Description
        A description of the config source.
     
    .PARAMETER ImportScript
        The scriptblock to execute to read configuration from the source.
     
    .PARAMETER ExportScript
        The scriptblock to execute to write configuration to the source.
     
    .EXAMPLE
        PS C:\> Register-MonConfigSource -Name 'Path' -Description 'Uses the filesystem as data backend for monitoring configuration' -ImportScript $ImportScript -ExportScript $ExportScript
     
        Registers the "Path" config source.
#>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [string]
        $Name,
        
        [Parameter(Mandatory = $true)]
        [string]
        $Description,
        
        [Parameter(Mandatory = $true)]
        [scriptblock]
        $ImportScript,
        
        [Parameter(Mandatory = $true)]
        [scriptblock]
        $ExportScript
    )
    
    process
    {
        $script:configSources[$Name] = @{
            Name         = $Name
            Description  = $Description
            ImportScript = $ImportScript
            ExportScript = $ExportScript
        }
    }
}

function Register-MonDataSource
{
<#
    .SYNOPSIS
        Registers a custom monitoring data source.
     
    .DESCRIPTION
        Registers a custom monitoring data source.
        Data sources are the data backend that manage the data gathered by the checks.
        For example, the 'Path' data source that comes with the module (and is the default source) will store the data in file.
        This command makes the actual backend freely extensible.
     
    .PARAMETER Name
        The name of the source. Must be unique, otherwise the previous data source will be overwritten,
     
    .PARAMETER Description
        A description of the data source.
     
    .PARAMETER ImportScript
        The scriptblock to execute to read data from the source.
     
    .PARAMETER ExportScript
        The scriptblock to execute to write data to the source.
     
    .EXAMPLE
        PS C:\> Register-MonDataSource -Name 'Path' -Description 'Uses the filesystem as data backend for monitoring data' -ImportScript $ImportScript -ExportScript $ExportScript
     
        Registers the "Path" data source.
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $Name,
        
        [Parameter(Mandatory = $true)]
        [string]
        $Description,
        
        [Parameter(Mandatory = $true)]
        [scriptblock]
        $ImportScript,
        
        [Parameter(Mandatory = $true)]
        [scriptblock]
        $ExportScript
    )
    process
    {
        $script:dataSources[$Name] = @{
            Name         = $Name
            Description  = $Description
            ImportScript = $ImportScript
            ExportScript = $ExportScript
        }
    }
}

function Get-MonTarget
{
<#
    .SYNOPSIS
        Returns registered monitoring targets.
     
    .DESCRIPTION
        Returns registered monitoring targets.
     
    .PARAMETER Name
        The name of the target.
     
    .PARAMETER Tag
        The tags the tartget should have.
     
    .EXAMPLE
        PS C:\> Get-MonTarget
     
        Returns all targets
#>

    [CmdletBinding()]
    Param (
        [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [string[]]
        $Name = "*",
        
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [string[]]
        $Tag = "*"
    )
    
    process
    {
        foreach ($targetItem in $script:configuration.Values)
        {
            $clonedItem = $targetItem.Clone()
            #region Filter by Name
            $foundName = $false
            foreach ($nameItem in $Name)
            {
                if ($clonedItem.Name -like $nameItem)
                {
                    $foundName = $true
                    break
                }
            }
            if (-not $foundName) { continue }
            #endregion Filter by Name
            
            #region Filter by Tag
            $foundTag = $false
            foreach ($tagItem in $Tag)
            {
                if ($clonedItem.Tag -like $tagItem)
                {
                    $foundTag = $true
                    break
                }
            }
            if (-not $foundTag) { continue }
            #endregion Filter by Tag
            
            $clonedItem['PSTypeName'] = 'Monitoring.Target'
            [pscustomobject]$clonedItem
        }
    }
}


function Remove-MonTarget
{
<#
    .SYNOPSIS
        Deletes a target.
     
    .DESCRIPTION
        Deletes a target.
        This will purge all configuration data from memory and file.
        Optionally, the data gathered from checks can be retained.
     
    .PARAMETER Name
        Name of the target to purge.
     
    .PARAMETER KeepData
        By default, all data pertaining a given target will be purged as well.
        Using this switch disables that behavior.
     
    .EXAMPLE
        PS C:\> Remove-MonTarget -Name 'server.contoso.com'
     
        Removes the target named server.contoso.com
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [string[]]
        $Name,
        
        [switch]
        $KeepData
    )
    
    process
    {
        foreach ($nameItem in $Name)
        {
            Import-Config -TargetName $nameItem
            if (-not $script:configuration[$nameItem])
            {
                Write-Error "Unable to find target $nameItem"
                continue
            }
            $script:configuration.Remove($nameItem)
            if ($script:limits.ContainsKey($nameItem)) { $script:limits.Remove($nameItem) }
            Export-Config -TargetName $nameItem
            
            Import-Data -TargetName $nameItem
            if (-not $KeepData -and $script:data[$nameItem])
            {
                $script:data.Remove($nameItem)
                Export-Data -TargetName $nameItem
            }
        }
    }
}


function Set-MonTarget
{
<#
    .SYNOPSIS
        Register a monitoring target.
     
    .DESCRIPTION
        Register a monitoring target.
        This is used for updating and maintaining a target system.
     
    .PARAMETER Name
        What to target the monitoring subject by.
        A unique label used to identify the resource.
        For computer management, this could be a DNS name.
        For monitoring SQL instances a connection string or instance name.
     
    .PARAMETER Tag
        What tag to assign to the target.
        Tags are monitoring groups. Checks are assigned to tags.
        For example, all checks assigned the tag 'DC' are applied to targets also tagged as 'DC'.
        At the same time, a target could also have the tag 'Server' and thus be subject to checks assigned to that tag.
        Assignments of tags are cummulative - applying new tags adds them to the object.
     
    .PARAMETER Capability
        Capabilities are used to determine what kind of connections can be established to this target.
        For example, adding a WinRM capability would tell the system the target can accept PowerShell Remoting and CIM over WinRM connections.
        Add supported capabilities by using the Register-MonConnection.
        Assignments of capabilities are cummulative - applying new capabilities adds them to the object.
     
    .EXAMPLE
        PS C:\> Set-MonTarget -Name 'server1.contoso.com' -Tag 'server', 'dc', 'server2019' -Capability WinRM, ldap
     
        Configures the server 'server1.contoso.com' as server, server2019 and dc.
        It also configures it to accept WinRM and ldap connections.
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSPossibleIncorrectUsageOfAssignmentOperator", "")]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [string[]]
        $Name,
        
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string[]]
        $Tag,
        
        [ValidateSet('WinRM', 'LDAP')]
        [string[]]
        $Capability
    )
    
    process
    {
        foreach ($nameItem in $Name)
        {
            Import-Config -TargetName $nameItem
            #region Add to existing target
            if ($target = $script:configuration[$nameItem])
            {
                foreach ($tagItem in $Tag)
                {
                    if ($target.Tag -notcontains $tagItem)
                    {
                        $target.Tag + $tagItem
                    }
                }
                foreach ($capabilityItem in $Capability)
                {
                    if ($target.Capability -notcontains $capabilityItem)
                    {
                        $target.Capability + $capabilityItem
                    }
                }
            }
            #endregion Add to existing target
            
            #region Create new target
            else
            {
                $script:configuration[$nameItem] = @{
                    Name       = $nameItem
                    Tag           = $Tag
                    Capability = $Capability
                }
            }
            #endregion Create new target
            Export-Config -TargetName $nameItem
        }
    }
}


<#
This is an example configuration file
 
By default, it is enough to have a single one of them,
however if you have enough configuration settings to justify having multiple copies of it,
feel totally free to split them into multiple files.
#>


<#
# Example Configuration
Set-PSFConfig -Module 'Monitoring' -Name 'Example.Setting' -Value 10 -Initialize -Validation 'integer' -Handler { } -Description "Example configuration setting. Your module can then use the setting using 'Get-PSFConfigValue'"
#>


Set-PSFConfig -Module 'Monitoring' -Name 'Import.DoDotSource' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be dotsourced on import. By default, the files of this module are read as string value and invoked, which is faster but worse on debugging."
Set-PSFConfig -Module 'Monitoring' -Name 'Import.IndividualFiles' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be imported individually. During the module build, all module code is compiled into few files, which are imported instead by default. Loading the compiled versions is faster, using the individual files is easier for debugging and testing out adjustments."

# Data Settings
Set-PSFConfig -Module 'Monitoring' -Name 'Data.StaleTimeout' -Value (New-TimeSpan -Minutes 30) -Initialize -Validation 'timespan' -Description "The time limit after which data is by default considered to be stale."

# Runspace Settings
Set-PSFConfig -Module 'Monitoring' -Name 'Runspace.MaxWorkerCount' -Value 32 -Initialize -Validation 'integer' -Description "The maximum number of targets that can be processed in parallel."
Set-PSFConfig -Module 'Monitoring' -Name 'Runspace.ExecutionTimeout' -Value (New-TimeSpan -Seconds 300) -Initialize -Validation 'timespan' -Description "The timeout of each worker runspace. If gathering data takes longer than this, data gathering is cancelled."

# Source Settings
Set-PSFConfig -Module 'Monitoring' -Name 'Source.Config.Active' -Value 'Path' -Initialize -Validation 'string' -Description 'Which data source is used for configuration storage and retrieval.'
Set-PSFConfig -Module 'Monitoring' -Name 'Source.Data.Active' -Value 'Path' -Initialize -Validation 'string' -Description 'Which data source is used for data storage and retrieval.'

# Management Pack Settings
Set-PSFConfig -Module 'Monitoring' -Name 'ManagementPack.AutoLoad' -Value $false -Initialize -Validation 'bool' -Description 'Whether to automatically detect and load Management Pack Modules during module import.'
Set-PSFConfig -Module 'Monitoring' -Name 'ManagementPack.Import' -Value @() -Initialize -Validation 'stringarray' -Description 'An explicit list of Management Pack Modules to import when importing this module.'

<#
Stored scriptblocks are available in [PsfValidateScript()] attributes.
This makes it easier to centrally provide the same scriptblock multiple times,
without having to maintain it in separate locations.
 
It also prevents lengthy validation scriptblocks from making your parameter block
hard to read.
 
Set-PSFScriptblock -Name 'Monitoring.ScriptBlockName' -Scriptblock {
     
}
#>


Register-PSFTeppScriptblock -Name Monitoring.Tags -ScriptBlock {
    (Get-MonCheck).Tag, (Get-MonTarget).Tag | Remove-PSFNUll -Enumerate | Select-Object -Unique | Sort-Object
}

Register-PSFTeppScriptblock -Name Monitoring.Target -ScriptBlock {
    (Get-MonTarget).Name
}

Register-PSFTeppScriptblock -Name Monitoring.Check -ScriptBlock {
    if ($fakeBoundParameter.TargetName)
    {
        $targetTags = (Get-MonTarget -Name $fakeBoundParameter.TargetName).Tag
        if ($targetTags) { return (Get-MonCheck -Tag $targetTags).Name }
    }
    (Get-MonCheck).Name
}

Register-PSFTeppScriptblock -Name Monitoring.Connection -ScriptBlock {
    (Get-MonConnection).Name
}

<#
# Example:
Register-PSFTeppScriptblock -Name "Monitoring.alcohol" -ScriptBlock { 'Beer','Mead','Whiskey','Wine','Vodka','Rum (3y)', 'Rum (5y)', 'Rum (7y)' }
#>


Register-PSFTeppArgumentCompleter -Command Get-MonCheck -Parameter Tag -Name 'Monitoring.Tags'
Register-PSFTeppArgumentCompleter -Command Get-MonTarget -Parameter Tag -Name 'Monitoring.Tags'
Register-PSFTeppArgumentCompleter -Command Invoke-MonCheck -Parameter Tag -Name 'Monitoring.Tags'
Register-PSFTeppArgumentCompleter -Command Register-MonCheck -Parameter Tag -Name 'Monitoring.Tags'
Register-PSFTeppArgumentCompleter -Command Set-MonTarget -Parameter Tag -Name 'Monitoring.Tags'
Register-PSFTeppArgumentCompleter -Command Test-MonHealth -Parameter Tag -Name 'Monitoring.Tags'

Register-PSFTeppArgumentCompleter -Command Disconnect-MonTarget -Parameter TargetName -Name 'Monitoring.Target'
Register-PSFTeppArgumentCompleter -Command Get-MonDatum -Parameter TargetName -Name 'Monitoring.Target'
Register-PSFTeppArgumentCompleter -Command Get-MonLimit -Parameter TargetName -Name 'Monitoring.Target'
Register-PSFTeppArgumentCompleter -Command Get-MonTarget -Parameter Name -Name 'Monitoring.Target'
Register-PSFTeppArgumentCompleter -Command Invoke-MonCheck -Parameter TargetName -Name 'Monitoring.Target'
Register-PSFTeppArgumentCompleter -Command Remove-MonTarget -Parameter Name -Name 'Monitoring.Target'
Register-PSFTeppArgumentCompleter -Command Set-MonLimit -Parameter TargetName -Name 'Monitoring.Target'
Register-PSFTeppArgumentCompleter -Command Set-MonTarget -Parameter Name -Name 'Monitoring.Target'
Register-PSFTeppArgumentCompleter -Command Test-MonHealth -Parameter TargetName -Name 'Monitoring.Target'

Register-PSFTeppArgumentCompleter -Command Get-MonDatum -Parameter CheckName -Name 'Monitoring.Check'
Register-PSFTeppArgumentCompleter -Command Get-MonLimit -Parameter CheckName -Name 'Monitoring.Check'
Register-PSFTeppArgumentCompleter -Command Set-MonLimit -Parameter CheckName -Name 'Monitoring.Check'
Register-PSFTeppArgumentCompleter -Command Test-MonHealth -Parameter CheckName -Name 'Monitoring.Check'

Register-PSFTeppArgumentCompleter -Command Set-MonTarget -Parameter Capability -Name 'Monitoring.Connection'

New-PSFLicense -Product 'Monitoring' -Manufacturer 'Friedrich Weinmann' -ProductVersion $script:ModuleVersion -ProductType Module -Name MIT -Version "1.0.0.0" -Date (Get-Date "2019-04-15") -Text @"
Copyright (c) 2019 Friedrich Weinmann
 
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
 
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
 
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"@


#if (-not (Test-Path -Path $script:pathConfiguration)) { New-Item -Path $script:pathConfiguration -ItemType Directory -Force }

#region Data & Config
# DATA: Stores the data actually present on each target for each applicable check
$script:data = @{
    <#
        TargetName = @{
            CheckName = @{
                Timestamp = $null
                Result = $null
                Message = ""
            }
        }
    #>

}

# CONFIG: Stores configuration data, specifically that relating to targets
$script:configuration = @{
<#
    TargetName = @{
        Name = 'TargetName'
        Tag = 'tag1','tag2'
        Capability = 'WinRM'
    }
#>

}

# CONFIG: Stores the limits specified by the monitoring system.
$script:limits = @{
    <#
    TargetName = @{
        CheckName = @{
            TargetName = 'TargetName'
            CheckName = 'CheckName'
            AlarmLimit = $null
            WarningLimit = $null
            Operator = 'GreaterThan|GreaterEqual|Equal|NotEqual|LesserEqual|LesserThan|Like|NotLike|Match|NotMatch'
        }
    }
    #>

}
#endregion Data & Config

#region Runtime
# RUNTIME : Stores the registered config source configuration
$script:configSources = @{
    <#
    SourceName = @{
        Name = 'SourceName'
        ImportScript = { ... }
        ExportScript = { ... }
    }
    #>

}

# RUNTIME : Stores the registered data source configuration
$script:dataSources = @{
    <#
    SourceName = @{
        Name = 'SourceName'
        ImportScript = { ... }
        ExportScript = { ... }
    }
    #>

}

# RUNTIME : Stores the various checks that have been registered
$script:checks = @{
    <#
    CheckName = @{
        Name = 'CheckName'
        Tag = 'tag1'
        Check = { ... }
    }
    #>

}

# RUNTIME: Stores registered connection types
$script:connectionTypes = @{
    <#
    CapabilityName = @{
        Name = 'CapabilityName'
        Connect = { ... }
        Disconnect = { ... }
    }
    #>

}
#endregion Runtime

$script:triedAutoImport = $false

Register-MonConnection -Capability ldap -ConnectionScript {
    param (
        $TargetName
    )
    # Nothing - it's a dummy connection
    @{ }
} -DisconnectionScript {
    param (
        $Connections,
        
        $TargetName
    )
    # Nothing - it's a dummy connection
}

Register-MonConnection -Capability WinRM -ConnectionScript {
    param (
        $TargetName
    )
    
    @{
        'WinRM_PS' = (New-PSSession -ComputerName $TargetName)
        'WinRM_CIM' = (New-CimSession -ComputerName $TargetName)
    }
} -DisconnectionScript {
    param (
        $Connections,
        
        $TargetName
    )
    
    if ($Connections.WinRM_PS) { $Connections.WinRM_PS | Remove-PSSession }
    if ($Connections.WinRM_CIM) { $Connections.WinRM_CIM | Remove-CimSession }
}

#region Configurations
$basePath = Join-PSFPath $env:APPDATA 'WindowsPowerShell' 'Monitoring'
if (Test-PSFPowerShell -Elevated) { $basePath = Join-PSFPath $env:ProgramData 'WindowsPowerShell' 'Monitoring' }
Set-PSFConfig -Module 'Monitoring' -Name 'Source.Path.Config' -Value (Join-Path -Path $basePath -ChildPath Config) -Initialize -Validation 'string' -Description 'The path where the "Path" data source stores its configuration data.'
#endregion Configurations

#region Create Configuration Source
$scriptblockImport = {
    param (
        [string]
        $Type,
        
        [string]
        $TargetName
    )
    
    #region Import Targets
    if ($Type -match '^All$|^Target$')
    {
        foreach ($fileItem in (Get-Item "$(Get-PSFConfigValue -FullName 'Monitoring.Source.Path.Config')\*.target"))
        {
            $baseName = $fileItem.BaseName | ConvertFrom-Base64
            
            if (-not (($baseName -eq $TargetName) -or ($baseName -like $TargetName))) { continue }
            
            $data = Import-PSFClixml -Path $fileItem.FullName
            $script:configuration[$data.Target] = $data.Value
        }
    }
    #endregion Import Targets
    
    #region Import Limits
    if ($Type -match '^All$|^Limit$')
    {
        foreach ($fileItem in (Get-Item "$(Get-PSFConfigValue -FullName 'Monitoring.Source.Path.Config')\*.limit"))
        {
            $baseName = $fileItem.BaseName | ConvertFrom-Base64
            
            if (-not (($baseName -eq $TargetName) -or ($baseName -like $TargetName))) { continue }
            
            $data = Import-PSFClixml -Path $fileItem.FullName
            $script:limits[$data.Target] = $data.Value
        }
    }
    #endregion Import Limits
}
$scriptblockExport = {
    param (
        [string]
        $Type,
        
        [string]
        $TargetName
    )
    
    $pathConfiguration = Get-PSFConfigValue -FullName 'Monitoring.Source.Path.Config'
    
    #region Export Targets
    if ($Type -match '^All$|^Target$')
    {
        $wasFound = $false
        foreach ($key in $script:configuration.Keys)
        {
            if (-not (($key -eq $TargetName) -or ($key -like $TargetName))) { continue }
            if ($key -eq $TargetName) { $wasFound = $true }
            
            $object = [pscustomobject]@{
                Target   = $key
                Value    = $script:configuration[$key]
            }
            $object | Export-PSFClixml -Path (Join-Path -Path $pathConfiguration -ChildPath "$($key | ConvertTo-Base64).target") -Depth 5
        }
        
        # Delete logic. Applies when using Remove-MonTarget
        if ((-not $wasFound) -and (Test-Path (Join-Path -Path $pathConfiguration -ChildPath "$($TargetName | ConvertTo-Base64).target")))
        {
            Remove-Item -Path (Join-Path -Path $pathConfiguration -ChildPath "$($TargetName | ConvertTo-Base64).target") -Force
        }
    }
    #endregion Export Targets
    
    #region Export Limits
    if ($Type -match '^All$|^Limit$')
    {
        $wasFound = $false
        foreach ($key in $script:limits.Keys)
        {
            if (-not (($key -eq $TargetName) -or ($key -like $TargetName))) { continue }
            if ($key -eq $TargetName) { $wasFound = $true }
            
            $object = [pscustomobject]@{
                Target   = $key
                Value    = $script:limits[$key]
            }
            $object | Export-PSFClixml -Path (Join-Path -Path $pathConfiguration -ChildPath "$($key | ConvertTo-Base64).limit") -Depth 5
        }
        
        # Delete logic. Applies when using Remove-MonTarget
        if ((-not $wasFound) -and (Test-Path (Join-Path -Path $pathConfiguration -ChildPath "$($TargetName | ConvertTo-Base64).limit")))
        {
            Remove-Item -Path (Join-Path -Path $pathConfiguration -ChildPath "$($TargetName | ConvertTo-Base64).limit") -Force
        }
    }
    #endregion Export Limits
}
$paramRegisterMonConfigSource = @{
    Name         = 'Path'
    Description  = 'Uses the filesystem as data backend for monitoring configuration'
    ImportScript = $scriptblockImport
    ExportScript = $scriptblockExport
}
Register-MonConfigSource @paramRegisterMonConfigSource
#endregion Create Configuration Source

#region Ensure Path Exists
$pathConfigCache = Get-PSFConfigValue -FullName 'Monitoring.Source.Path.Config'
if (-not (Test-Path -Path $pathConfigCache))
{
    $null = New-Item -Path $pathConfigCache -ItemType Directory -Force
}
#endregion Ensure Path Exists

#region Configurations
$basePath = Join-PSFPath $env:APPDATA 'WindowsPowerShell' 'Monitoring'
if (Test-PSFPowerShell -Elevated) { $basePath = Join-PSFPath $env:ProgramData 'WindowsPowerShell' 'Monitoring' }
Set-PSFConfig -Module 'Monitoring' -Name 'Source.Path.Data' -Value (Join-Path -Path $basePath -ChildPath Data) -Initialize -Validation 'string' -Description 'The path where the "Path" data source stores its scan content.'
#endregion Configurations

#region Create Data Source
$scriptblockImport = {
    param (
        [string]
        $TargetName = '*'
    )
    
    $wasFound = $false
    foreach ($fileItem in (Get-ChildItem -Path (Get-PSFConfigValue -FullName 'Monitoring.Source.Path.Data') -File))
    {
        $baseName = $fileItem.BaseName | ConvertFrom-Base64
        
        if ($baseName -eq $TargetName) { $wasFound = $true }
        if (($baseName -eq $TargetName) -or ($baseName -like $TargetName))
        {
            $importedData = Import-PSFClixml -Path $fileItem.FullName
            $script:data[$importedData.Target] = $importedData.Content
        }
    }
    if (-not $TargetName.Contains("*") -and -not $wasFound)
    {
        $script:data[$TargetName] = @{ }
    }
}
$scriptblockExport = {
    param (
        [string]
        $TargetName = '*'
    )
    
    $pathDataCache = Get-PSFConfigValue -FullName 'Monitoring.Source.Path.Data'
    $wasFound = $false
    
    foreach ($key in $script:data.Keys)
    {
        if (($key -ne $TargetName) -and ($key -notlike $TargetName)) { continue }
        if ($key -eq $TargetName) { $wasFound = $true }
        
        $object = [pscustomobject]@{
            Target   = $key
            Content  = $script:data[$key]
        }
        $object | Export-PSFClixml -Path (Join-Path -Path $pathDataCache -ChildPath ($key | ConvertTo-Base64)) -Depth 5
    }
    
    # Delete logic. Applies when using Remove-MonTarget
    if ((-not $wasFound) -and (Test-Path (Join-Path -Path $pathDataCache -ChildPath ($TargetName | ConvertTo-Base64))))
    {
        Remove-Item -Path (Join-Path -Path $pathDataCache -ChildPath ($TargetName | ConvertTo-Base64)) -Force
    }
}
$paramRegisterMonDataSource = @{
    Name         = 'Path'
    Description  = 'Uses the filesystem as data backend for monitoring data'
    ImportScript = $scriptblockImport
    ExportScript = $scriptblockExport
}
Register-MonDataSource @paramRegisterMonDataSource
#endregion Create Data Source

#region Ensure Path Exists
$pathDataCache = Get-PSFConfigValue -FullName 'Monitoring.Source.Path.Data'
if (-not (Test-Path -Path $pathDataCache))
{
    $null = New-Item -Path $pathDataCache -ItemType Directory -Force
}
#endregion Ensure Path Exists

# Load the persisted target configuration
Import-Config -Type Target
#endregion Load compiled code