Elastic.Helper.psm1

# Convert to Newline Delimited JSON
function ConvertTo-NdJson {
  <#
  .SYNOPSIS
  Convert JSON/PSObject to Newline Delimited JSON (NDJSON) for bulk imports to ES
  .DESCRIPTION
  This function is intended to reformat a normal JSON array as a series of NDJSON lines. Will also work in a Pipeline
  .PARAMETER Input
  Input Array of PSObject
  #>


  [cmdletbinding()]
  param (
  # Input Data as PSObject to be converted to Newline Delimited JSON Entries
    [parameter(Mandatory = $true,
      ValueFromPipeline = $true)]
      $Input
  )

  Process {
    $Results = @()

    foreach ($item in $Input) {

      $Results += $item | ConvertTo-Json -Compress -Depth 8

    }

    Write-Output $Results
  }
}

Function Test-Guid {
  <#
  .SYNOPSIS
  Determine if a given string is a valid Guid.
  .DESCRIPTION
  Check for a valid Guid
  .PARAMETER Guid
  The string to be tested.
  .INPUTS
  System.String - Guid to be tested
  .OUTPUTS
  System.Boolean
    True if string is a valid Guid
    False if string is not a valid Guid
  .EXAMPLE
  PS > "edea82e3-8d0b-4370-86f0-d96bcd4b6c19" | Test-Guid
  True
  .EXAMPLE
  PS > Test-Guid "edea82e3-8d0b-4370-86f0-d96bcd4b6c19"
  True
  .LINK
  https://github.com/LogRhythm-Tools/LogRhythm.Tools
  #>


  [CmdletBinding()]
  [OutputType([System.Boolean])]

  Param(
    [Parameter(Mandatory = $false, Position = 0, ValueFromPipeline = $true)]
    [string] $Guid
  )

  Process {
    if ([string]::IsNullOrEmpty($Guid)) {
      return $false
    }
    $ValidGuid = [guid]::Empty
      if (! ([guid]::TryParse($Guid, [ref]$ValidGuid))) {
      return $false
      }
    return $true
  }
}

# Import the Elastic Helper Configuration

function Get-EsHelperConfig {
  <#
  .SYNOPSIS
    Import an Elastic Helper configuration.
  .DESCRIPTION
    Import an Elastic Helper configuration JSON file from a stored location.
  .PARAMETER ConfigName
    BaseName of Configuration. To determine the actual filename, the '.json' extension is appended.
  .PARAMETER Path
    (Optional) Path to configuration directory.
 
    Defaults to .eshelper in the user's home directory.
 
    On Windows this will be determined by Group Policy, but defaults to C:\Users\<username>
 
    On Linux this will be determined by OS configuration, but is usually /home/<username>
 
    On Mac OS/X this is usually /Users/<username>
  .INPUTS
    System.String -> Configuration Name
  .OUTPUTS
    PSCustomObject (Hash) representing the configuration file contents
  .EXAMPLE
    Load Configuration file 'elasticproject.json' from default folder into the $EsConf variable
 
    PS C:\> $EsConf = Get-EsHelperConfig -ConfigName elasticproject
  .EXAMPLE
    Load Configuration file 'elasticproject.json' from '/opt/scripts/project/etc'
 
    PS C:\> $EsConf = Get-EsHelperConfig -ConfigName elasticproject -Path '/opt/scripts/project/etc'
  .LINK
    https://github.com/IPSecMSSP/Elastic.Helper
  #>


  [CmdletBinding()]

  param (
    [string] [Parameter(Mandatory=$true)] $ConfigName,
    [string] [Parameter(Mandatory=$false)] $Path = [io.path]::Combine($HOME,".eshelper")
  )

  # Build the full path to the config file
  $ConfigFileName = "{0}.json" -f $ConfigName
  $ConfigFilePath = [io.path]::Combine($Path, $ConfigFileName)

  Get-Content -Path $ConfigFilePath | ConvertFrom-Json -depth 10
}

# Get Index Definition from config
<#
.SYNOPSIS
  Get the definition from the named index from the supplied configuration object
.DESCRIPTION
  Provide the desired definition from the configuration file for the specified index
.PARAMETER EsConfig
  PSObject containing the read in configuration
.PARAMETER IndexName
  Name of ElasticSearch Index to return definition for
#>

function Get-EsIndexDefinition {
  param (
    [PSCustomObject] [Parameter(Mandatory=$true)] $EsConfig,
    [string] [Parameter(Mandatory=$true)] $IndexName,
    [switch] [Parameter(Mandatory=$false)] $Exact
  )

  foreach ($Index in $EsConfig.Indices) {
    if ($Exact) {
      if ($IndexName -eq $Index.name) {
      return $Index
      }
    } else {
      if ($IndexName -match $Index.name) {
      return $Index
      }
    }
  }
}

# Test for Enrich Policy Dependencies
function Test-EsEnrichPolicyDepends {
    <#
    .SYNOPSIS
        Test if the Enrichment Policy's Dependencies are met.
    .DESCRIPTION
        Using the provided configuration definition, determine if it's dependencies are met in ElasticSearch.
 
        Will check all defined enrichment policies unless one is specified.
 
        Checks ElasticSearch to confirm that the index on which the policy is based exists.
    .PARAMETER EsConfig
        ElasticHelper configuration loaded using Get-EsHelperConfig.
    .PARAMETER PolicyName
        (Optional) Name of Enrichment Policy to check for unmet dependencies.
 
        If not specified, checks all defined Enrichment Policies.
    .PARAMETER EsCreds
        PSCredential object containing username and password to access ElasticSearch
    .INPUTS
        PSCustomObject -> Defined ElasticHelper Configuration
    .OUTPUTS
        Success (1) or Failure (0)
    .EXAMPLE
        Test all defined Enrichment Policies for Dependencies
 
        PS C:\> $Result = Test-EsEnrichmentPolicyDepends -EsConfig $EsConfig
    .EXAMPLE
        Test the MyEnrichmentPolicy Enrichment Policy for Dependencies
 
        PS C:\> $Result = Test-EsEnrichmentPolicyDepends -EsConfig $EsConfig -PolicyName 'MyEnrichmentPolicy'
    .LINK
        https://github.com/IPSecMSSP/Elastic.Helper
    #>


  [CmdletBinding()]

  param(
    [PSCustomObject] [Parameter(Mandatory=$true)] $EsConfig,
    [string] [Parameter(Mandatory=$false)] $PolicyName,
    [PSCustomObject] [Parameter(Mandatory=$false)] $EsCreds
  )

# Find the Enrichment Policy in the config

  foreach ($Policy in $EsConfig._enrich.policies) {
    # Check all policies if none specified, otherwise check just the one
    if(-not ($PSBoundParameters.ContainsKey('PolicyName')) -or $Policy.name -eq $PolicyName) {
      $EsIndex = $Policy.definition.match.indices
      Write-Debug " Checking Index: $EsIndex"
      if ($EsCreds) {
        $EsIndexStatus = Get-EsIndex -ESUrl $EsConfig.eshome -EsIndex $EsIndex -EsCreds $EsCreds
      } else {
        $EsIndexStatus = Get-EsIndex -ESUrl $EsConfig.eshome -EsIndex $EsIndex
      }

      Write-Debug "Index Status: $EsIndexStatus"
      if($EsIndexStatus.Error) {
        if ($EsIndexStatus.status -eq 404) {
            $msg = "Unmet Dependency: Index {0} not found" -f $EsIndex
        } else {
            $msg = "Error: {0}" -f $esIndexStatus.Error
        }
        Write-Output $msg
      } elseif ($EsIndexStatus.$EsIndex) {
        Write-Debug " Index $EsIndex Present..."
        if ($EsCreds) {
          $RunningPolicy = Get-EsEnrichmentPolicy -ESUrl $EsConfig.eshome -Policy $Policy.name -EsCreds $EsCreds | ConvertFrom-Json -Depth 8
        } else {
          $RunningPolicy = Get-EsEnrichmentPolicy -ESUrl $EsConfig.eshome -Policy $Policy.name | ConvertFrom-Json -Depth 8
        }
        if ($RunningPolicy.policies.Count -eq 0) {
          $msg = "Unmet Dependency: Enrichment Policy {0} not loaded" -f $Policy.name
          Write-Error $msg
        } else {
          $msg = "Dependency Met: Enrichment Policy {0} loaded" -f $Policy.name
          Write-Debug $msg
        }
      }
    }
  }
}

# Test for Enrich Policy Dependencies
function Test-EsEnrichPolicyExists {
    <#
    .SYNOPSIS
        Test if the Enrichment Policy Exists.
    .DESCRIPTION
        Using the provided configuration definition, determine if the enrichment policy exists in ElasticSearch.
 
        Will check all defined enrichment policies unless one is specified.
 
        Checks ElasticSearch to confirm that the index on which the policy is based exists.
    .PARAMETER EsConfig
        ElasticHelper configuration loaded using Get-EsHelperConfig.
    .PARAMETER PolicyName
        (Optional) Name of Enrichment Policy to check for existence.
 
        If not specified, checks all defined Enrichment Policies.
    .PARAMETER EsCreds
        PSCredential object containing username and password to access ElasticSearch
    .INPUTS
        PSCustomObject -> Defined ElasticHelper Configuration
    .OUTPUTS
        Success (1) or Failure (0)
    .EXAMPLE
        Test all defined Enrichment Policies for existence
 
        PS C:\> $Result = Test-EsEnrichmentPolicyExists -EsConfig $EsConfig
    .EXAMPLE
        Test the MyEnrichmentPolicy Enrichment Policy for existence
 
        PS C:\> $Result = Test-EsEnrichmentPolicyExists -EsConfig $EsConfig -PolicyName 'MyEnrichmentPolicy'
    .LINK
        https://github.com/IPSecMSSP/Elastic.Helper
    #>


  [CmdletBinding()]

  param(
    [PSCustomObject] [Parameter(Mandatory=$true)] $EsConfig,
    [string] [Parameter(Mandatory=$false)] $PolicyName,
    [PSCustomObject] [Parameter(Mandatory=$false)] $EsCreds
  )

# Find the Enrichment Policy in the config

  foreach ($Policy in $EsConfig._enrich.policies) {
    # Check all policies if none specified, otherwise check just the one
    if(-not ($PSBoundParameters.ContainsKey('PolicyName')) -or $Policy.name -eq $PolicyName) {
      if ($EsCreds) {
        $RunningPolicy = Get-EsEnrichmentPolicy -ESUrl $EsConfig.eshome -Policy $Policy.name -EsCreds $EsCreds
      } else {
        $RunningPolicy = Get-EsEnrichmentPolicy -ESUrl $EsConfig.eshome -Policy $Policy.name
      }
      if ($RunningPolicy.policies.Count -eq 0) {
        $msg = "Unmet Dependency: Enrichment Policy {0} not loaded" -f $Policy.name
        Write-Error $msg
        Write-Output $False
      } else {
        $msg = "Dependency Met: Enrichment Policy {0} loaded" -f $Policy.name
        Write-Verbose $msg
        Write-Output $True
      }
      Write-Debug $RunningPolicy | ConvertTo-Json
    }
  }
}

# Test Index dependencies
function Test-EsIndexDepends {
  <#
  .SYNOPSIS
  Test if the Index's Dependencies are met.
  .DESCRIPTION
  Using the provided configuration definition, determine if it's dependencies are met in ElasticSearch.
 
  Will check all defined enrichment indices unless one is specified.
 
  Checks ElasticSearch to confirm that the pipeline, and associated dependencies, configured for the index exists.
  .PARAMETER EsConfig
  ElasticHelper configuration loaded using Get-EsHelperConfig.
  .PARAMETER IndexName
  (Optional) Name of Index to check for unmet dependencies.
 
  If not specified, checks all defined Indices.
  .PARAMETER EsCreds
  PSCredential object containing username and password to access ElasticSearch
  .INPUTS
  PSCustomObject -> Defined ElasticHelper Configuration
  .OUTPUTS
  Success (1) or Failure (0)
  .EXAMPLE
  Test all defined Indices for Dependencies
 
  PS C:\> $Result = Test-EsIndexDepends -EsConfig $EsConfig
  .EXAMPLE
  Test the MyIndex Index for Dependencies
 
  PS C:\> $Result = Test-EsIndexDepends -EsConfig $EsConfig -IndexName 'MyIndex'
  .LINK
  https://github.com/IPSecMSSP/Elastic.Helper
  #>


  [CmdletBinding()]
  [OutputType([Int32])]

  param (
  [PSCustomObject] [Parameter(Mandatory=$true)] $EsConfig,
  [string] [Parameter(Mandatory=$false)] $IndexName,
  [PSCustomObject] [Parameter(Mandatory=$false)] $EsCreds
  )

  foreach ($Index in $EsConfig.indices) {
     # Check all indices if no index name is specified, otherwise check just the one index
    if ( -not($PSBoundParameters.ContainsKey('IndexName')) -or $Index.name -eq $IndexName ) {
      if ($Index.pipeline) {
        $PipelineDepends = Test-EsPipelineDepends -EsConfig $EsConfig -PipelineName $Index.pipeline
        if ($null -eq $PipelineDepends) {
          # Dependencies matched
          if ($EsCreds) {
            $EsIndexStatus = Get-EsIndex -ESUrl $EsConfig.eshome -EsIndex $Index.name -EsCreds $EsCreds
          } else {
            $EsIndexStatus = Get-EsIndex -ESUrl $EsConfig.eshome -EsIndex $Index.name
          }

          if($EsIndexStatus.Error) {
            if ($EsIndexStatus.status -eq 404) {
              $msg = "Unmet Dependency: Index {0} not found" -f $Index.name
            } else {
              $msg = "Error: {0}" -f $EsIndexStatus.Error
            }
            Write-Output $msg
          } elseif ($EsIndexStatus.{$Index.name}) {
            return 1
          }

        }
      }
    }
  }
}

# Test Index dependencies
function Test-EsIndexExists {
  <#
  .SYNOPSIS
    Test if the Index exists.
  .DESCRIPTION
    Using the provided configuration definition, determine if the index exists in ElasticSearch.
 
    Will check all defined indices unless one is specified.
 
    Checks ElasticSearch to confirm that the index exists.
  .PARAMETER EsConfig
    ElasticHelper configuration loaded using Get-EsHelperConfig.
  .PARAMETER IndexName
    (Optional) Name of Index to check for existence.
 
    If not specified, checks all defined Indices.
  .PARAMETER EsCreds
    PSCredential object containing username and password to access ElasticSearch
  .INPUTS
    PSCustomObject -> Defined ElasticHelper Configuration
  .OUTPUTS
    $true if it exists, $false if not
  .EXAMPLE
    Test all defined Indices for existence
 
    PS C:\> $Result = Test-EsIndexExists -EsConfig $EsConfig
  .EXAMPLE
    Test the MyIndex Index for Existence
 
    PS C:\> $Result = Test-EsIndexExists -EsConfig $EsConfig -IndexName 'MyIndex'
  .LINK
    https://github.com/IPSecMSSP/Elastic.Helper
  #>


  [CmdletBinding()]
  [OutputType([boolean])]

  param (
    [PSCustomObject] [Parameter(Mandatory=$true)] $EsConfig,
    [string] [Parameter(Mandatory=$false)] $IndexName,
    [PSCustomObject] [Parameter(Mandatory=$false)] $EsCreds
  )

  foreach ($Index in $EsConfig.indices) {
     # Check all indices if no index name is specified, otherwise check just the one index
    if ( -not($PSBoundParameters.ContainsKey('IndexName')) -or $Index.name -eq $IndexName ) {
      Write-Verbose "Testing existence of index: $($Index.name)"
      $IndexParams = @{}
      $IndexParams.Add('ESUrl',$EsConfig.eshome)
      if ($EsCreds) {
        $IndexParams.Add('EsCreds',$EsCreds)
      }

      if ($Index.indexpattern) {
        $IndexParams.Add('EsIndex', $Index.indexpattern)
      } else {
        $IndexParams.Add('EsIndex', $Index.name)
      }

      $EsIndexStatus = Get-EsIndex @IndexParams

      if($EsIndexStatus.Error) {
        if ($EsIndexStatus.status -eq 404) {
          $msg = "Index {0} not found" -f $Index.name
        } else {
          $msg = "Error: {0}" -f $EsIndexStatus.Error
        }
        Write-Error $msg
        Write-Output $false
      } elseif ($EsIndexStatus.PSObject.Properties.Name -match $Index.name) {
        Write-verbose "Index exists: $($Index.name)"
        Write-Output $true
      }

      Write-Debug ($EsIndexStatus | ConvertTo-Json -depth 10)

    }
  }
}

# Test Pipeline Dependencies
function Test-EsPipelineDepends {
    <#
    .SYNOPSIS
        Test if the Pipeline's Dependencies are met.
    .DESCRIPTION
        Using the provided configuration definition, determine if it's dependencies are met in ElasticSearch.
 
        Will check all defined pipelines unless one is specified.
 
        Checks ElasticSearch to confirm that the pipeline, and associated dependencies exist.
    .PARAMETER EsConfig
        ElasticHelper configuration loaded using Get-EsHelperConfig.
    .PARAMETER PipelineName
        (Optional) Name of Pipeline to check for unmet dependencies.
 
        If not specified, checks all defined Pipelines.
    .PARAMETER EsCreds
        PSCredential object containing username and password to access ElasticSearch
    .INPUTS
        PSCustomObject -> Defined ElasticHelper Configuration
    .OUTPUTS
        Success (1) or Failure (0)
    .EXAMPLE
        Test all defined Pipelines for Dependencies
 
        PS C:\> $Result = Test-EsPipelineDepends -EsConfig $EsConfig
    .EXAMPLE
        Test the MyPipeline Pipeline for Dependencies
 
        PS C:\> $Result = Test-EsPipelineDepends -EsConfig $EsConfig -PipelineName 'MyPipeline'
    .LINK
        https://github.com/IPSecMSSP/Elastic.Helper
    #>


  [CmdletBinding()]

  param(
    [PsCustomObject] [Parameter(Mandatory=$true)] $EsConfig,
    [string] [Parameter(Mandatory=$false)] $PipelineName,
    [PSCustomObject] [Parameter(Mandatory=$false)] $EsCreds
  )

  # Find the pipeline in the config
  foreach($Pipeline in $EsConfig._ingest.pipelines) {
    # Check all pipelines if none specified, otherwise check just the one
    if (-not ($PSBoundParameters.ContainsKey('PipelineName')) -or $Pipeline.name -eq $PipelineName) {
      foreach($PipelineProcessor in $Pipeline.definition.processors) {
        if ($PipelineProcessor.enrich) {
          $EnrichPolicy = $PipelineProcessor.enrich.policy_name
          if ($EsCreds) {
            $EnrichPolicyDepends = Test-EsEnrichPolicyDepends -EsConfig $EsConfig -PolicyName $EnrichPolicy -EsCreds $EsCreds
          } else {
            $EnrichPolicyDepends = Test-EsEnrichPolicyDepends -EsConfig $EsConfig -PolicyName $EnrichPolicy
          }
          if ($null -eq $EnrichPolicyDepends){
            # Dependencies matched
            if ($EsCreds) {
              $PipelineStatus = Get-EsPipeline -ESUrl $EsConfig.eshome -Pipeline $Pipeline.name -EsCreds $EsCreds|  ConvertFrom-Json -Depth 8
            } else {
              $PipelineStatus = Get-EsPipeline -ESUrl $EsConfig.eshome -Pipeline $Pipeline.name | ConvertFrom-Json -Depth 8
            }
            if($PipelineStatus.($Pipeline.name)) {
              # Pipeline is there
            } else {
              $msg = "Unmet Dependency: Pipeline {0} not loaded" -f $Pipeline.name
              Write-Output $msg
            }
          } else {
            $msg = "Unmet Dependencies for Pipeline: {0} - " -f $Pipeline.Name
            $msg += $EnrichPolicyDepends
            Write-Output $msg
          }
        }
      }
    }
  }
}

# Test Pipeline Dependencies
function Test-EsPipelineExists {
  <#
  .SYNOPSIS
      Test if the Pipeline exists.
  .DESCRIPTION
      Using the provided configuration definition, determine if it exists in ElasticSearch.
 
      Will check all defined pipelines unless one is specified.
 
      Checks ElasticSearch to confirm that the pipeline exists.
  .PARAMETER EsConfig
      ElasticHelper configuration loaded using Get-EsHelperConfig.
  .PARAMETER PipelineName
      (Optional) Name of Pipeline to check for existence.
 
      If not specified, checks all defined Pipelines.
  .PARAMETER EsCreds
      PSCredential object containing username and password to access ElasticSearch
  .INPUTS
      PSCustomObject -> Defined ElasticHelper Configuration
  .OUTPUTS
      $true if pipeline exists, $false if not
  .EXAMPLE
      Test all defined Pipelines for existence
 
      PS C:\> $Result = Test-EsPipelineExists -EsConfig $EsConfig
  .EXAMPLE
      Test the MyPipeline Pipeline for existence
 
      PS C:\> $Result = Test-EsPipelineExists -EsConfig $EsConfig -PipelineName 'MyPipeline'
  .LINK
      https://github.com/IPSecMSSP/Elastic.Helper
  #>


  [CmdletBinding()]

  param(
    [PsCustomObject] [Parameter(Mandatory=$true)] $EsConfig,
    [string] [Parameter(Mandatory=$false)] $PipelineName,
    [PSCustomObject] [Parameter(Mandatory=$false)] $EsCreds
  )

  # Find the pipeline in the config
  foreach($Pipeline in $EsConfig._ingest.pipelines) {
    # Check all pipelines if none specified, otherwise check just the one
    if (-not ($PSBoundParameters.ContainsKey('PipelineName')) -or $Pipeline.name -eq $PipelineName) {
      Write-Verbose "Testing existence of Pipeline: $($Pipeline.name)"
      if ($EsCreds) {
        $PipelineStatus = Get-EsPipeline -ESUrl $EsConfig.eshome -Pipeline $Pipeline.name -EsCreds $EsCreds
      } else {
        $PipelineStatus = Get-EsPipeline -ESUrl $EsConfig.eshome -Pipeline $Pipeline.name -EsCreds $EsCreds
      }

      if ($PipelineStatus.PSObject.Properties.Name -Match ($Pipeline.name)) {
        Write-Verbose "Pipeline exists: $($Pipeline.name)"
        Write-Output $true
      } else {
        Write-Verbose "Pipeline does not exist: $($Pipeline.name)"
        Write-Output $false
      }

      Write-Debug ($PipelineStatus | ConvertTo-Json -depth 10)
    }
  }
}

# Deploy ElasticSearch Configuration
# But only for resources that do not have dependencies, or where these dependencies are met
function Deploy-EsConfig {
  <#
  .SYNOPSIS
    Deploy specified ElasticSearch Resources to ElasticSearch cluster, with dependency checks.
  .DESCRIPTION
    Using the provided configuration definition, deploy the defined resources to ElasticSearch.
 
    Will check all dependencies are met before deploying to ElasticSeach, and will not attempt deployment of resources for which dependencies are not met.
 
    This may be used iteratively to defined the required resouces in ElasticSearch as data is populated.
  .PARAMETER EsConfig
    ElasticHelper configuration loaded using Get-EsHelperConfig.
  .PARAMETER ResourceType
    (Not Yet Implemented)
    (Optional) Type of Resources to deploy to ElasticSearch.
 
    One of: (index, pipeline, enrichmentpolicy)
 
    If not specified, attempts to deploy all resources without unmet dependencies.
  .PARAMETER ResourceName
    (Not Yet Implemented)
    (Optional) Name of Resource to deploy to ElasticSearch. If specified, must also specify resource type.
 
    If not specified, attempts to deploy all resources without unmet dependencies.
  .PARAMETER EsCreds
    PSCredential object containing username and password to access ElasticSearch
  .INPUTS
    PSCustomObject -> Defined ElasticHelper Configuration
  .OUTPUTS
    Success (1) or Failure (0)
  .EXAMPLE
    Deploy all defined Resources to ElasticSearch
 
    PS C:\> $Result = Deploy-EsConfig -EsConfig $EsConfig
  .EXAMPLE
    Load Configuration file 'elasticproject.json' from '/opt/scripts/project/etc'
 
    PS C:\> $EsConf = Get-EsHelperConfig -ConfigName elasticproject -Path '/opt/scripts/project/etc'
  .LINK
    https://github.com/IPSecMSSP/Elastic.Helper
  #>


  [CmdletBinding()]

  param(
    [PSCustomObject] [Parameter(Mandatory=$true)] $EsConfig,
    [string] [Parameter(Mandatory=$false)] $ResourceType,
    [string] [Parameter(Mandatory=$false)] $ResourceName,
    [PSCustomObject] [Parameter(Mandatory=$false)] $EsCreds
  )

  if ($PSBoundParameters.ContainsKey('ResourceType')) {
    if($PSBoundParameters.ContainsKey('ResourceName')) {
      # Process just the single Resource
    } else {
      # Can't have resource Type without Resource name
      Write-Error "Both ResourceType and ResourceName need to be specified"
    }
  } else {
    # In order, we need to do:
    # * Pipeline
    # * Index
    # * Enrichment Policy

    # Pipelines
    foreach ($Pipeline in $EsConfig._ingest.pipelines) {
      # Check dependencies
      if ($EsCreds) {
        $PipelineStatus = Test-EsPipelineDepends -EsConfig $EsConfig -PipelineName $Pipeline.name -EsCreds $EsCreds
      } else {
        $PipelineStatus = Test-EsPipelineDepends -EsConfig $EsConfig -PipelineName $Pipeline.name
      }
      if ($null -eq $PipelineStatus) {
        # Dependency check passed
        $msg = "Deploying Pipeline: {0}" -f $PipeLine.name
        Write-Output $msg
        if ($EsCreds) {
          $result = Update-EsPipeline -ESUrl $EsConfig.eshome -Pipeline $Pipeline.name -PipelineDefinition ($PipeLine.definition | ConvertTo-Json -Depth 8) -EsCreds $EsCreds
        } else {
          $result = Update-EsPipeline -ESUrl $EsConfig.eshome -Pipeline $Pipeline.name -PipelineDefinition ($PipeLine.definition | ConvertTo-Json -Depth 8)
        }
        if ($result) {
          $msg = "Pipeline Deployed: {0}" -f $PipeLine.name
        } else {
          $msg = "Pipeline Deployment Failed: {0} - {1}" -f $PipeLine.name, $result
        }
        Write-Output $msg
      } else {
        # Dependency check failed
        $msg = "Pipeline Deployment Skipped: {0} - Dependencies not met.`n {1}" -f $PipeLine.name, $PipelineStatus
        Write-Output $msg
      }
    }

    # Indices
    # We don't actually deploy indices, these are written by the data insert process

    # Enrichment Policies
    foreach ($Policy in $EsConfig._enrich.policies) {
      # Check Dependencies
      if ($EsCreds) {
        $PolicyStatus = Test-EsEnrichPolicyDepends -EsConfig $EsConfig -PolicyName $Policy.name -EsCreds $EsCreds
      } else {
        $PolicyStatus = Test-EsEnrichPolicyDepends -EsConfig $EsConfig -PolicyName $Policy.name
      }
      if ($null -eq $PolicyStatus) {
        # Dependency Check Passed
        $msg = "Deploying Enrichment Policy: {0}" -f $Policy.name
        Write-Output $msg
        if ($EsCreds) {
          $result = Update-EsEnrichmentPolicy -ESUrl $EsConfig.eshome -Policy $Policy.name -PolicyDefinition ($Policy.definition | ConvertTo-Json -Depth 8) -EsCreds $EsCreds
        } else {
          $result = Update-EsEnrichmentPolicy -ESUrl $EsConfig.eshome -Policy $Policy.name -PolicyDefinition ($Policy.definition | ConvertTo-Json -Depth 8)
        }
        if ($result) {
          $msg = "Enrichment Policy Deployed: {0}" -f $Policy.name
        } else {
          $msg = "Enrichment Policy Deployment Failed: {0} - {1}" -f $Policy.name, $result
        }
        Write-Output $msg
      } else {
        # Dependency Check failed
        $msg = "Enrichment Policy Deployment Skipped: {0} - Dependencies not met.`n {1}" -f $Policy.name, $PolicyStatus
        Write-Output $msg
      }
    }
  }
}

# Get the current enrichment policy definition
function Get-EsEnrichmentPolicy {
  <#
  .SYNOPSIS
      Get the currently configured Enrichment Policy configuration on the ElasticSearch server for the specified policy name
  .DESCRIPTION
      Get the configuration of the specified Enrichment Policy from the nomiated ElasticSearch server.
 
      Optionally supports Authentication.
  .PARAMETER EsUrl
      Base URL for your ElasticSearch server/cluster
  .PARAMETER Policy
      Name of ElasticSearch Enrichment Policy to get current configuration of.
  .PARAMETER EsCreds
      PSCredential object containing username and password to access ElasticSearch
  .INPUTS
      None
  .OUTPUTS
      Definition of current Enrichment Policy on ElasticSearch Cluster
  .EXAMPLE
      Retrieve Enrichment Policy named MyEnrichmentPolicy without authentication
 
      PS C:\> $EnrichPol = Get-EsEnrichmentPolicy -EsUrl http://192.168.1.10:9200 -Policy 'MyEnrichmentPolicy'
  .LINK
      https://github.com/IPSecMSSP/Elastic.Helper
  #>


  [CmdletBinding()]

  param (
    [string] [Parameter(Mandatory=$true)] $ESUrl,
    [string] [Parameter(Mandatory=$true)] $Policy,
    [PSCustomObject] [Parameter(Mandatory=$false)] $EsCreds
  )

  $Method = 'GET'
  $Uri = [io.path]::Combine($ESUrl, "_enrich/policy/", $Policy)

  if ($EsCreds){
    Invoke-Elasticsearch -Uri $Uri -Method $Method -ContentType 'application/json' -User $EsCreds.Username -Password $EsCreds.Password -SkipCertificateCheck | ConvertFrom-Json -Depth 10
  } else {
    Invoke-Elasticsearch -Uri $Uri -Method $Method -ContentType 'application/json' -SkipCertificateCheck | ConvertFrom-Json -Depth 10
  }

}

# Rebuild Enrichment Indices
function Update-EsEnrichmentIndices {
  <#
    .SYNOPSIS
        Rebuild all Enrichment indices associated with the specified Enrichment Policy
    .DESCRIPTION
        Each time the base/source index for an Enrichment Policy has documents added or updated, the system indices used to perform enrichment lookups need to be rebuilt.
 
        This operation triggers this task on the cluster.
 
        Optionally supports Authentication.
    .PARAMETER EsUrl
        Base URL for your ElasticSearch server/cluster
    .PARAMETER Policy
        Name of Enrichment Policy to rebuild enrichment indices for
    .PARAMETER EsCreds
        PSCredential object containing username and password to access ElasticSearch
    .INPUTS
        None
    .OUTPUTS
        Result of requested operation
    .EXAMPLE
        Rebuld enrichment indices associated to 'MyEnrichmentPolicy'
 
        PS C:\> $result = Update-EsEnrichmentIndices -EsUrl http://192.168.1.10:9200 -Policy 'MyEnrichmentPolicy'
    .LINK
        https://github.com/IPSecMSSP/Elastic.Helper
  #>



  [CmdletBinding(SupportsShouldProcess)]

  param (
    [string] [Parameter(Mandatory=$true)] $ESUrl,
    [string] [Parameter(Mandatory=$true)] $Policy,
    [PSCustomObject] [Parameter(Mandatory=$false)] $EsCreds
  )

  $Method = 'POST'
  $Uri = [io.path]::Combine($ESUrl, "_enrich/policy/", $Policy, "_execute")

  if($PSCmdlet.ShouldProcess($Uri)) {
    if ($EsCreds) {
      $Result = Invoke-Elasticsearch -Uri $Uri -Method $Method -ContentType 'application/json' -User $EsCreds.UserName -Password $EsCreds.Password  -SkipCertificateCheck| ConvertFrom-Json -depth 8
    } else {
      $Result = Invoke-Elasticsearch -Uri $Uri -Method $Method -ContentType 'application/json'  -SkipCertificateCheck| ConvertFrom-Json -depth 8
    }
  }

  return $Result
}

# Execute Enrichment Policies tied to a specified index
function Update-EsEnrichmentIndicesFromIndex {
  <#
  .SYNOPSIS
    Test if the Index's Dependencies are met.
  .DESCRIPTION
    Each time the base/source index for an Enrichment Policy has documents added or updated, the system indices used to perform enrichment lookups need to be rebuilt.
 
    This operation triggers this task on the cluster, based on the index that is used in an enrichment policy.
 
    Optionally supports Authentication.
  .PARAMETER EsConfig
    ElasticHelper configuration loaded using Get-EsHelperConfig.
  .PARAMETER IndexName
    (Optional) Name of Index to check for unmet dependencies.
 
    If not specified, checks all defined Indices.
  .PARAMETER EsCreds
    PSCredential object containing username and password to access ElasticSearch
  .INPUTS
    PSCustomObject -> Defined ElasticHelper Configuration
  .OUTPUTS
    Success (1) or Failure (0)
  .EXAMPLE
    Rebuld enrichment indices associated to 'MyIndex'
 
    PS C:\> $result = Update-EsEnrichmentIndicesFromIndex -EsUrl http://192.168.1.10:9200 -IndexName 'MyIndex'
  .LINK
    https://github.com/IPSecMSSP/Elastic.Helper
  #>


  [CmdletBinding(SupportsShouldProcess)]

  param(
  [PSCustomObject] [Parameter(Mandatory=$true)] $EsConfig,
  [string] [Parameter(Mandatory=$true)] $IndexName,
  [PSCustomObject] [Parameter(Mandatory=$false)] $EsCreds
)

# Look through the Enrich Policies in our configuration
foreach ($Policy in $EsConfig._enrich.policies) {
  # Find those that depend on our index
  if ($IndexName -match $Policy.definition.match.indices) {
    if ($PSCmdlet.ShouldProcess($Policy.name)) {
      # Update the Enrichment Index
      $msg = "Updating Enrichment Policy Index - Index: {0}; Policy: {1};" -f $IndexName, $Policy.name
      Write-Debug $msg

      # Sleep briefly to allow Index to quiesce
      Start-Sleep -Seconds 1
      if ($EsCreds) {
        $result = Update-EsEnrichmentIndices -ESUrl $EsConfig.eshome -Policy $Policy.name -EsCreds $EsCreds
      } else {
        $result = Update-EsEnrichmentIndices -ESUrl $EsConfig.eshome -Policy $Policy.name
      }
    }
    Write-Debug $result
  }
}
}

# Update the Enrichment Policy
function Update-EsEnrichmentPolicy {
  <#
  .SYNOPSIS
      Update the definition of the specified Enrichment Policy with the provided definition.
  .DESCRIPTION
      Use the supplied policy definition to update the running policy for the specified enrichment policy.
 
      Optionally supports Authentication.
  .PARAMETER EsUrl
      Base URL for your ElasticSearch server/cluster
  .PARAMETER Policy
      Name of ElasticSearch Enrichment Policy to get current configuration of.
  .PARAMETER PolicyDefinition
      PSCustomObject defining the desired state of the policy definition
  .PARAMETER EsCreds
      PSCredential object containing username and password to access ElasticSearch
  .INPUTS
      None
  .OUTPUTS
      Definition of current Enrichment Policy on ElasticSearch Cluster
  .EXAMPLE
      Update the Enrichment Policy named MyEnrichmentPolicy without authentication
 
      PS C:\> $EnrichPol = Update-EsEnrichmentPolicy -EsUrl http://192.168.1.10:9200 -Policy 'MyEnrichmentPolicy' -PolicyDefinition $PolicyDef
  .LINK
      https://github.com/IPSecMSSP/Elastic.Helper
  #>


  [CmdletBinding(SupportsShouldProcess)]

  param (
    [string] [Parameter(Mandatory=$true)] $ESUrl,
    [string] [Parameter(Mandatory=$true)] $Policy,
    [Parameter(Mandatory=$true)] $PolicyDefinition,
    [PSCustomObject] [Parameter(Mandatory=$false)] $EsCreds
  )

  $Method = 'PUT'
  $Uri = [io.path]::Combine($ESUrl, "_enrich/policy/", $Policy)
  $EsBody = [Elastic.ElasticsearchRequestBody] $PolicyDefinition

  if($PSCmdlet.ShouldProcess($Uri)) {
    if ($EsCreds) {
      $Result = Invoke-Elasticsearch -Uri $Uri -Method $Method -ContentType 'application/json' -Body $EsBody -User $EsCreds.UserName -Password $EsCreds.Password  -SkipCertificateCheck| ConvertFrom-Json -depth 8
    } else {
      $Result = Invoke-Elasticsearch -Uri $Uri -Method $Method -ContentType 'application/json' -Body $EsBody  -SkipCertificateCheck| ConvertFrom-Json -depth 8
    }
  }

  return $Result.acknowledged
}

# Convert to ElasticSearch Bulk Index request
function ConvertTo-EsBulkIndex {
  <#
  .SYNOPSIS
    Convert Array of JDJSON lines to interspersed Index requests followed by data to be indexed
  .DESCRIPTION
    This function is intended to turn a series of NDJSON lines into an ES Bulk Index request. Will also work in a Pipeline
  .PARAMETER Input
    Input Array of PSObject
  .PARAMETER Index
    Name of index, may be overridden by Pipeline configuration
  .PARAMETER Pipeline
    Name of Ingest Pipeline
  #>

    [OutputType([string])]
  [cmdletbinding()]
  param (
    # Input Data as Array of entries to be converted to Newline Delimited JSON Entries with specific base index
    [parameter(Mandatory = $true, ValueFromPipeline = $true)]  $Input,
    [parameter(Mandatory = $true)] $Index,
    [parameter(Mandatory = $true)] $Pipeline
  )

  Process {
    $Results = @()

    $Results += '{"index" : { "_index": "' + $Index + '", "pipeline": "' + $Pipeline + '"}}'
    $Results += ($Input | ConvertTo-Json -Compress -depth 8) -replace [char] 0x00a0,'-'

    Write-Output $Results

  }
  End {
    # Ensure newline at the end of the request
    Write-Output ''
  }

}

# Get index info

function Get-EsIndex {
  <#
  .SYNOPSIS
      Get the currently configured Index configuration on the ElasticSearch server for the specified index name
  .DESCRIPTION
      Get the configuration of the specified Index from the nomiated ElasticSearch server.
 
      Optionally supports Authentication.
  .PARAMETER EsUrl
      Base URL for your ElasticSearch server/cluster
  .PARAMETER EsIndex
      Name of ElasticSearch Index to get current information and configuration of.
  .PARAMETER EsCreds
      PSCredential object containing username and password to access ElasticSearch
  .INPUTS
      None
  .OUTPUTS
      Information about the specified index on ElasticSearch Cluster
  .EXAMPLE
      Retrieve status and configuration about an index named MyIndex without authentication
 
      PS C:\> $EsIndex = Get-EsIndex -EsUrl http://192.168.1.10:9200 -EsIndex 'MyIndex'
  .LINK
      https://github.com/IPSecMSSP/Elastic.Helper
  #>


  [CmdletBinding()]

  param(
    [string] [Parameter(Mandatory=$true)] $ESUrl,
    [string] [Parameter(Mandatory=$true)] $EsIndex,
    [PSCustomObject] [Parameter(Mandatory=$false)] $EsCreds
  )

  $Method = 'GET'
  $Uri = [io.path]::Combine($ESUrl, $EsIndex, "_settings")

  if ($EsCreds) {
    Invoke-Elasticsearch -Uri $Uri -Method $Method -ContentType 'application/json' -User $EsCreds.UserName -Password $EsCreds.Password  -SkipCertificateCheck| ConvertFrom-Json -Depth 8
  } else {
    Invoke-Elasticsearch -Uri $Uri -Method $Method -ContentType 'application/json' -SkipCertificateCheck | ConvertFrom-Json -Depth 8
  }
}

function Get-EsIndexSettings {
  <#
  .SYNOPSIS
    Get the currently configured Index configuration on the ElasticSearch server for the specified index name
  .DESCRIPTION
    Get the configuration of the specified Index from the nomiated ElasticSearch server.
 
    Optionally supports Authentication.
  .PARAMETER EsUrl
    Base URL for your ElasticSearch server/cluster
  .PARAMETER IndexName
    Name of ElasticSearch Index to get current information and configuration of.
  .PARAMETER EsCreds
    PSCredential object containing username and password to access ElasticSearch
  .INPUTS
    None
  .OUTPUTS
    Information about the specified index on ElasticSearch Cluster
  .EXAMPLE
    Retrieve status and configuration about an index named MyIndex without authentication
 
    PS C:\> $EsIndex = Get-EsIndex -EsUrl http://192.168.1.10:9200 -IndexName 'MyIndex'
  .LINK
    https://github.com/IPSecMSSP/Elastic.Helper
  #>


  [CmdletBinding()]

  param (
    [string] [Parameter(Mandatory=$true)] $ESUrl,
    [string] [Parameter(Mandatory=$true)] $IndexName,
    [PSCustomObject] [Parameter(Mandatory=$false)] $EsCreds
  )

  $Method = 'GET'
  $Uri = [io.path]::Combine($ESUrl, $IndexName, "_settings")
  if ($EsCreds) {
    Invoke-Elasticsearch -Uri $Uri -Method $Method -ContentType 'application/json' -User $EsCreds.UserName -Password $EsCreds.Password -SkipCertificateCheck
  } else {
    Invoke-Elasticsearch -Uri $Uri -Method $Method -ContentType 'application/json' -SkipCertificateCheck
  }
}

# Bulk Index logs
# Index some records
function Invoke-EsBulkIndexRequest {
  <#
  .SYNOPSIS
    Bulk Index an array of PSObjects to the specified ElasticSearch instance and index
  .DESCRIPTION
    Takes an array of PSObjects and submits it to the ElasticSearch instance as a bulk index request denoted by the EsConfig object and index name
  .PARAMETER EsConfig
    PSObject containing the read in configuration
  .PARAMETER IndexName
    Name of ElasticSearch Index to return definition for
  .PARAMETER InputArray
    Array of PSObjects to sumbit to the index
  #>

  param (
    [PSCustomObject] [Parameter(Mandatory=$true)] $EsConfig,
    [string] [Parameter(Mandatory=$true)] $IndexName,
    [Parameter(Mandatory=$true)] $InputObject,
    [PSCustomObject] [Parameter(Mandatory=$false)] $EsCreds,
    [int] [Parameter(Mandatory=$false)] $ChunkSize = 10000
  )

  Write-Debug "Processing Bulk Index Request..."

  $BulkIndexURI = '{0}/_bulk' -f $EsConfig.eshome, $IndexName

  Write-Debug " URI: $BulkIndexURI"

  # Break the task into chunks
  $Chunks = [System.Collections.ArrayList]::new()
  for ($i = 0; $i -lt $InputObject.Count; $i += $ChunkSize) {
    if (($InputObject.Count - $i) -gt ($ChunkSize -1)) {
      $Chunks.Add($InputObject[$i..($i + ($ChunkSize -1))])
    } else {
      $Chunks.Add($InputObject[$i..($InputObject.Count - 1)])
    }
  }

  foreach ($Chunk in $Chunks) {
    $ndjson = $Chunk | ConvertTo-EsBulkIndex -Index $IndexName -Pipeline (Get-EsIndexDefinition -EsConfig $EsConfig -IndexName $IndexName -Exact).pipeline

    if ($EsCreds) {
      Write-Debug " Credentials Supplied"
      $ndjson -join "`n" | Invoke-Elasticsearch -Uri $BulkIndexURI -Method POST -ContentType 'application/x-ndjson; charset=utf-8' -User $EsCreds.UserName -Password $EsCreds.Password -SkipCertificateCheck
    } else {
      Write-Debug " No Credentials Supplied"
      $ndjson -join "`n" | Invoke-Elasticsearch -Uri $BulkIndexURI -Method POST -ContentType 'application/x-ndjson; charset=utf-8' -SkipCertificateCheck
    }
  }

  # After indexing a bunch of records, update the enrichment indices (if any) associated to the index
  Write-Debug " Updating Enrichment Indices"
  if ($EsCreds) {
    Update-EsEnrichmentIndicesFromIndex -EsConfig $EsConfig -IndexName $IndexName -EsCreds $EsCreds
  } else {
    Update-EsEnrichmentIndicesFromIndex -EsConfig $EsConfig -IndexName $IndexName
  }

}

function Update-EsIndexSettings {
  <#
  .SYNOPSIS
    Update the settings for the specified index with the provided definition.
  .DESCRIPTION
    Use the supplied index definition to update the existing settings of the specified index.
 
    If additional settings are present on the ElasticSearch index, the new settings will merge with and override existing settings. Other settings will remain unchanged.
 
    This allows you to update just the 'number_of_replicas' setting without affecting any other settings.
 
    Optionally supports Authentication.
  .PARAMETER EsUrl
    Base URL for your ElasticSearch server/cluster
  .PARAMETER IndexName
    Name of ElasticSearch Index to update current configuration of.
  .PARAMETER IndexDefinition
    PSCustomObject defining the desired state of the index configuration
  .PARAMETER EsCreds
    PSCredential object containing username and password to access ElasticSearch
  .INPUTS
    None
  .OUTPUTS
    Result of operation
  .EXAMPLE
    Update the index named MyIndex without authentication
 
    PS C:\> $IndexDef = @{'index' = @{ 'number_of_replicas' = '0'} }
    PS C:\> $EnrichPol = Update-EsIndexSettings -EsUrl http://192.168.1.10:9200 -IndexName 'MyIndex' -IndexDefinition $IndexDef
  .LINK
    https://github.com/IPSecMSSP/Elastic.Helper
  #>


  [CmdletBinding(SupportsShouldProcess)]

  param (
    [string] [Parameter(Mandatory=$true)] $ESUrl,
    [string] [Parameter(Mandatory=$true)] $IndexName,
    [string] [Parameter(Mandatory=$true)] $IndexDefinition,
    [PSCustomObject] [Parameter(Mandatory=$false)] $EsCreds
  )

  $Method = 'PUT'
  $Uri = [io.path]::Combine($ESUrl, $IndexName, "_settings")
  $EsBody = [Elastic.ElasticsearchRequestBody] $IndexDefinition

  if($PSCmdlet.ShouldProcess($Uri)) {
    if ($EsCreds) {
      $Result = Invoke-Elasticsearch -Uri $Uri -Method $Method -ContentType 'application/json' -Body $EsBody -User $EsCreds.UserName -Password $EsCreds.Password  -SkipCertificateCheck| ConvertFrom-Json -depth 8
    } else {
      $Result = Invoke-Elasticsearch -Uri $Uri -Method $Method -ContentType 'application/json' -Body $EsBody  -SkipCertificateCheck| ConvertFrom-Json -depth 8
    }
  }

  return $Result.acknowledged

}

function Update-EsIndexSettingsFromConfig {
  <#
  .SYNOPSIS
    Update the ElasticSearch index settings with values from the config file
  .DESCRIPTION
    Use the supplied configuration object to update the ElasticSearch running environment.
 
    This will recursively identify required objects to create, such as Pipeline, Enrichment Policy, etc
 
    Optionally supports Authentication.
  .PARAMETER EsConfig
    PSCustomObject containing configuration data loaded with Get-EsHelperConfig
  .PARAMETER IndexName
    Name of ElasticSearch Index to update current configuration of.
  .PARAMETER EsCreds
    PSCredential object containing username and password to access ElasticSearch
  .INPUTS
    None
  .OUTPUTS
    Result of operation
  .EXAMPLE
    Update the index named MyIndex without authentication
 
    PS C:\> $EsConf = Get-EsHelperConfig -ConfigName 'esproject'
    PS C:\> $result = Update-EsIndexSettingsFromConfig -EsConf $EsConf -IndexName 'MyIndex'
  .LINK
    https://github.com/IPSecMSSP/Elastic.Helper
  #>


  [CmdletBinding(SupportsShouldProcess)]

  param (
    [PSCustomObject] [Parameter(Mandatory=$true)] $EsConfig,
    [string] [Parameter(Mandatory=$true)] $IndexName,
    [PSCustomObject] [Parameter(Mandatory=$false)] $EsCreds
  )

  foreach ($Index in $EsConfig.Indices) {
    if ($IndexName -match $Index.name) {
      If($PSCmdlet.ShouldProcess($IndexName)) {
        If ($EsCreds){
          Update-EsIndexSettings -ESUrl $EsConfig.eshome -IndexName $IndexName -IndexDefinition ($Index.settings | ConvertTo-Json -Depth 8) -EsCreds $EsCreds
        } else {
          Update-EsIndexSettings -ESUrl $EsConfig.eshome -IndexName $IndexName -IndexDefinition ($Index.settings | ConvertTo-Json -Depth 8)
        }
      }
    }
  }

}

# Get the current pipeline definition
function Get-EsPipeline {
  <#
  .SYNOPSIS
      Get the currently configured Pipeline configuration on the ElasticSearch server for the specified index name
  .DESCRIPTION
      Get the configuration of the specified Pipeline from the nomiated ElasticSearch server.
 
      Optionally supports Authentication.
  .PARAMETER EsUrl
      Base URL for your ElasticSearch server/cluster
  .PARAMETER Pipeline
      Name of ElasticSearch Pipeline to get current configuration of.
  .PARAMETER EsCreds
      PSCredential object containing username and password to access ElasticSearch
  .INPUTS
      None
  .OUTPUTS
      Configuration of specified pipeline on ElasticSearch Cluster
  .EXAMPLE
      Retrieve status and configuration about an pipeline named MyPipeline without authentication
 
      PS C:\> $EsPipeline = Get-EsPipeline -EsUrl http://192.168.1.10:9200 -Pipeline 'MyPipeline'
  .LINK
      https://github.com/IPSecMSSP/Elastic.Helper
  #>


  [CmdletBinding()]

  param (
    [string] [Parameter(Mandatory=$true)] $ESUrl,
    [string] [Parameter(Mandatory=$true)] $Pipeline,
    [PSCustomObject] [Parameter(Mandatory=$false)] $EsCreds
  )

  $Method = 'GET'
  $Uri = [io.path]::Combine($ESUrl, "_ingest/pipeline/", $Pipeline)
  if ($EsCreds) {
    Invoke-Elasticsearch -Uri $Uri -Method $Method -ContentType 'application/json' -User $EsCreds.Username -Password $EsCreds.Password -SkipCertificateCheck | ConvertFrom-Json -Depth 10
  } else {
    Invoke-Elasticsearch -Uri $Uri -Method $Method -ContentType 'application/json' -SkipCertificateCheck | ConvertFrom-Json -Depth 10
  }
}

# Update the pipeline definition with a new one
function Update-EsPipeline {
  <#
  .SYNOPSIS
      Update the settings for the specified Pipeline with the provided definition.
  .DESCRIPTION
      Use the supplied Pipeline definition to update the existing settings of the specified Pipeline.
 
      If additional settings are present on the ElasticSearch Pipeline, the new settings will merge with and override existing settings. Other settings will remain unchanged.
 
      This allows you to update just the 'number_of_replicas' setting without affecting any other settings.
 
      Optionally supports Authentication.
  .PARAMETER EsUrl
      Base URL for your ElasticSearch server/cluster
  .PARAMETER Pipeline
      Name of ElasticSearch Pipeline to update current configuration of.
  .PARAMETER PipelineDefinition
      PSCustomObject defining the desired state of the Pipeline configuration
  .PARAMETER EsCreds
      PSCredential object containing username and password to access ElasticSearch
  .INPUTS
      None
  .OUTPUTS
      Result of operation
  .EXAMPLE
      Update the Pipeline named MyPipeline without authentication
 
      PS C:\> $EnrichPol = Update-EsPipelineSettings -EsUrl http://192.168.1.10:9200 -Pipeline 'MyPipeline' -PipelineDefinition $PipelineDef
  .LINK
      https://github.com/IPSecMSSP/Elastic.Helper
  #>


  [CmdletBinding(SupportsShouldProcess)]

  param (
    [string] [Parameter(Mandatory=$true)] $ESUrl,
    [string] [Parameter(Mandatory=$true)] $Pipeline,
    [Parameter(Mandatory=$true)] $PipelineDefinition,
    [PSCustomObject] [Parameter(Mandatory=$false)] $EsCreds
  )

  $Method = 'PUT'
  $Uri = [io.path]::Combine($ESUrl, "_ingest/pipeline/", $Pipeline)
  $EsBody = [Elastic.ElasticsearchRequestBody] $PipelineDefinition

  if($PSCmdlet.ShouldProcess($Uri)) {
    if ($EsCreds) {
      $Result = Invoke-Elasticsearch -Uri $Uri -Method $Method -ContentType 'application/json' -Body $EsBody -User $EsCreds.UserName -Password $EsCreds.Password  -SkipCertificateCheck| ConvertFrom-Json -depth 8
    } else {
      $Result = Invoke-Elasticsearch -Uri $Uri -Method $Method -ContentType 'application/json' -Body $EsBody  -SkipCertificateCheck| ConvertFrom-Json -depth 8
    }
  }

  if ($Result.acknowledged) {
    # Successfully deployed Pipeline Config
    return $Result.acknowledged
  } else {
    return $Result
  }
}

Export-ModuleMember -Function Get-EsHelperConfig, Get-EsIndexDefinition, Test-EsEnrichPolicyDepends, Test-EsEnrichPolicyExists, Test-EsIndexDepends, Test-EsIndexExists, Test-EsPipelineDepends, Test-EsPipelineExists, Deploy-EsConfig, Get-EsEnrichmentPolicy, Update-EsEnrichmentIndices, Update-EsEnrichmentIndicesFromIndex, Update-EsEnrichmentPolicy, ConvertTo-EsBulkIndex, Get-EsIndex, Get-EsIndexSettings, Invoke-EsBulkIndexRequest, Update-EsIndexSettings, Update-EsIndexSettingsFromConfig, Get-EsPipeline, Update-EsPipeline

# SIG # Begin signature block
# MIIoHgYJKoZIhvcNAQcCoIIoDzCCKAsCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDlG4aeWhghJZg0
# OK0CWHBReya90Qbyb8XTG4pytAgZBKCCISEwggWNMIIEdaADAgECAhAOmxiO+dAt
# 5+/bUOIIQBhaMA0GCSqGSIb3DQEBDAUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQK
# EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNV
# BAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0yMjA4MDEwMDAwMDBa
# Fw0zMTExMDkyMzU5NTlaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy
# dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lD
# ZXJ0IFRydXN0ZWQgUm9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC
# ggIBAL/mkHNo3rvkXUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3E
# MB/zG6Q4FutWxpdtHauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKy
# unWZanMylNEQRBAu34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsF
# xl7sWxq868nPzaw0QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU1
# 5zHL2pNe3I6PgNq2kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJB
# MtfbBHMqbpEBfCFM1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObUR
# WBf3JFxGj2T3wWmIdph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6
# nj3cAORFJYm2mkQZK37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxB
# YKqxYxhElRp2Yn72gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5S
# UUd0viastkF13nqsX40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+x
# q4aLT8LWRV+dIPyhHsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjggE6MIIB
# NjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qYrhwP
# TzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzAOBgNVHQ8BAf8EBAMC
# AYYweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdp
# Y2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNv
# bS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwRQYDVR0fBD4wPDA6oDigNoY0
# aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENB
# LmNybDARBgNVHSAECjAIMAYGBFUdIAAwDQYJKoZIhvcNAQEMBQADggEBAHCgv0Nc
# Vec4X6CjdBs9thbX979XB72arKGHLOyFXqkauyL4hxppVCLtpIh3bb0aFPQTSnov
# Lbc47/T/gLn4offyct4kvFIDyE7QKt76LVbP+fT3rDB6mouyXtTP0UNEm0Mh65Zy
# oUi0mcudT6cGAxN3J0TU53/oWajwvy8LpunyNDzs9wPHh6jSTEAZNUZqaVSwuKFW
# juyk1T3osdz9HNj0d1pcVIxv76FQPfx2CWiEn2/K2yCNNWAcAgPLILCsWKAOQGPF
# mCLBsln1VWvPJ6tsds5vIy30fnFqI2si/xK4VC0nftg62fC2h5b9W9FcrBjDTZ9z
# twGpn1eqXijiuZQwggauMIIElqADAgECAhAHNje3JFR82Ees/ShmKl5bMA0GCSqG
# SIb3DQEBCwUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMx
# GTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRy
# dXN0ZWQgUm9vdCBHNDAeFw0yMjAzMjMwMDAwMDBaFw0zNzAzMjIyMzU5NTlaMGMx
# CzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMy
# RGlnaUNlcnQgVHJ1c3RlZCBHNCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcg
# Q0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDGhjUGSbPBPXJJUVXH
# JQPE8pE3qZdRodbSg9GeTKJtoLDMg/la9hGhRBVCX6SI82j6ffOciQt/nR+eDzMf
# UBMLJnOWbfhXqAJ9/UO0hNoR8XOxs+4rgISKIhjf69o9xBd/qxkrPkLcZ47qUT3w
# 1lbU5ygt69OxtXXnHwZljZQp09nsad/ZkIdGAHvbREGJ3HxqV3rwN3mfXazL6IRk
# tFLydkf3YYMZ3V+0VAshaG43IbtArF+y3kp9zvU5EmfvDqVjbOSmxR3NNg1c1eYb
# qMFkdECnwHLFuk4fsbVYTXn+149zk6wsOeKlSNbwsDETqVcplicu9Yemj052FVUm
# cJgmf6AaRyBD40NjgHt1biclkJg6OBGz9vae5jtb7IHeIhTZgirHkr+g3uM+onP6
# 5x9abJTyUpURK1h0QCirc0PO30qhHGs4xSnzyqqWc0Jon7ZGs506o9UD4L/wojzK
# QtwYSH8UNM/STKvvmz3+DrhkKvp1KCRB7UK/BZxmSVJQ9FHzNklNiyDSLFc1eSuo
# 80VgvCONWPfcYd6T/jnA+bIwpUzX6ZhKWD7TA4j+s4/TXkt2ElGTyYwMO1uKIqjB
# Jgj5FBASA31fI7tk42PgpuE+9sJ0sj8eCXbsq11GdeJgo1gJASgADoRU7s7pXche
# MBK9Rp6103a50g5rmQzSM7TNsQIDAQABo4IBXTCCAVkwEgYDVR0TAQH/BAgwBgEB
# /wIBADAdBgNVHQ4EFgQUuhbZbU2FL3MpdpovdYxqII+eyG8wHwYDVR0jBBgwFoAU
# 7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoG
# CCsGAQUFBwMIMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0cDovL29j
# c3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0cy5kaWdp
# Y2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8EPDA6MDig
# NqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9v
# dEc0LmNybDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1sBwEwDQYJKoZI
# hvcNAQELBQADggIBAH1ZjsCTtm+YqUQiAX5m1tghQuGwGC4QTRPPMFPOvxj7x1Bd
# 4ksp+3CKDaopafxpwc8dB+k+YMjYC+VcW9dth/qEICU0MWfNthKWb8RQTGIdDAiC
# qBa9qVbPFXONASIlzpVpP0d3+3J0FNf/q0+KLHqrhc1DX+1gtqpPkWaeLJ7giqzl
# /Yy8ZCaHbJK9nXzQcAp876i8dU+6WvepELJd6f8oVInw1YpxdmXazPByoyP6wCeC
# RK6ZJxurJB4mwbfeKuv2nrF5mYGjVoarCkXJ38SNoOeY+/umnXKvxMfBwWpx2cYT
# gAnEtp/Nh4cku0+jSbl3ZpHxcpzpSwJSpzd+k1OsOx0ISQ+UzTl63f8lY5knLD0/
# a6fxZsNBzU+2QJshIUDQtxMkzdwdeDrknq3lNHGS1yZr5Dhzq6YBT70/O3itTK37
# xJV77QpfMzmHQXh6OOmc4d0j/R0o08f56PGYX/sr2H7yRp11LB4nLCbbbxV7HhmL
# NriT1ObyF5lZynDwN7+YAN8gFk8n+2BnFqFmut1VwDophrCYoCvtlUG3OtUVmDG0
# YgkPCr2B2RP+v6TR81fZvAT6gt4y3wSJ8ADNXcL50CN/AAvkdgIm2fBldkKmKYcJ
# RyvmfxqkhQ/8mJb2VVQrH4D6wPIOK+XW+6kvRBVK5xMOHds3OBqhK/bt1nz8MIIG
# sDCCBJigAwIBAgIQCK1AsmDSnEyfXs2pvZOu2TANBgkqhkiG9w0BAQwFADBiMQsw
# CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu
# ZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQw
# HhcNMjEwNDI5MDAwMDAwWhcNMzYwNDI4MjM1OTU5WjBpMQswCQYDVQQGEwJVUzEX
# MBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0IFRydXN0
# ZWQgRzQgQ29kZSBTaWduaW5nIFJTQTQwOTYgU0hBMzg0IDIwMjEgQ0ExMIICIjAN
# BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1bQvQtAorXi3XdU5WRuxiEL1M4zr
# PYGXcMW7xIUmMJ+kjmjYXPXrNCQH4UtP03hD9BfXHtr50tVnGlJPDqFX/IiZwZHM
# gQM+TXAkZLON4gh9NH1MgFcSa0OamfLFOx/y78tHWhOmTLMBICXzENOLsvsI8Irg
# nQnAZaf6mIBJNYc9URnokCF4RS6hnyzhGMIazMXuk0lwQjKP+8bqHPNlaJGiTUyC
# EUhSaN4QvRRXXegYE2XFf7JPhSxIpFaENdb5LpyqABXRN/4aBpTCfMjqGzLmysL0
# p6MDDnSlrzm2q2AS4+jWufcx4dyt5Big2MEjR0ezoQ9uo6ttmAaDG7dqZy3SvUQa
# khCBj7A7CdfHmzJawv9qYFSLScGT7eG0XOBv6yb5jNWy+TgQ5urOkfW+0/tvk2E0
# XLyTRSiDNipmKF+wc86LJiUGsoPUXPYVGUztYuBeM/Lo6OwKp7ADK5GyNnm+960I
# HnWmZcy740hQ83eRGv7bUKJGyGFYmPV8AhY8gyitOYbs1LcNU9D4R+Z1MI3sMJN2
# FKZbS110YU0/EpF23r9Yy3IQKUHw1cVtJnZoEUETWJrcJisB9IlNWdt4z4FKPkBH
# X8mBUHOFECMhWWCKZFTBzCEa6DgZfGYczXg4RTCZT/9jT0y7qg0IU0F8WD1Hs/q2
# 7IwyCQLMbDwMVhECAwEAAaOCAVkwggFVMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYD
# VR0OBBYEFGg34Ou2O/hfEYb7/mF7CIhl9E5CMB8GA1UdIwQYMBaAFOzX44LScV1k
# TN8uZz/nupiuHA9PMA4GA1UdDwEB/wQEAwIBhjATBgNVHSUEDDAKBggrBgEFBQcD
# AzB3BggrBgEFBQcBAQRrMGkwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2lj
# ZXJ0LmNvbTBBBggrBgEFBQcwAoY1aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29t
# L0RpZ2lDZXJ0VHJ1c3RlZFJvb3RHNC5jcnQwQwYDVR0fBDwwOjA4oDagNIYyaHR0
# cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZFJvb3RHNC5jcmww
# HAYDVR0gBBUwEzAHBgVngQwBAzAIBgZngQwBBAEwDQYJKoZIhvcNAQEMBQADggIB
# ADojRD2NCHbuj7w6mdNW4AIapfhINPMstuZ0ZveUcrEAyq9sMCcTEp6QRJ9L/Z6j
# fCbVN7w6XUhtldU/SfQnuxaBRVD9nL22heB2fjdxyyL3WqqQz/WTauPrINHVUHmI
# moqKwba9oUgYftzYgBoRGRjNYZmBVvbJ43bnxOQbX0P4PpT/djk9ntSZz0rdKOtf
# JqGVWEjVGv7XJz/9kNF2ht0csGBc8w2o7uCJob054ThO2m67Np375SFTWsPK6Wrx
# oj7bQ7gzyE84FJKZ9d3OVG3ZXQIUH0AzfAPilbLCIXVzUstG2MQ0HKKlS43Nb3Y3
# LIU/Gs4m6Ri+kAewQ3+ViCCCcPDMyu/9KTVcH4k4Vfc3iosJocsL6TEa/y4ZXDlx
# 4b6cpwoG1iZnt5LmTl/eeqxJzy6kdJKt2zyknIYf48FWGysj/4+16oh7cGvmoLr9
# Oj9FpsToFpFSi0HASIRLlk2rREDjjfAVKM7t8RhWByovEMQMCGQ8M4+uKIw8y4+I
# Cw2/O/TOHnuO77Xry7fwdxPm5yg/rBKupS8ibEH5glwVZsxsDsrFhsP2JjMMB0ug
# 0wcCampAMEhLNKhRILutG4UI4lkNbcoFUCvqShyepf2gpx8GdOfy1lKQ/a+FSCH5
# Vzu0nAPthkX0tGFuv2jiJmCG6sivqf6UHedjGzqGVnhOMIIGwDCCBKigAwIBAgIQ
# DE1pckuU+jwqSj0pB4A9WjANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJVUzEX
# MBUGA1UEChMORGlnaUNlcnQsIEluYy4xOzA5BgNVBAMTMkRpZ2lDZXJ0IFRydXN0
# ZWQgRzQgUlNBNDA5NiBTSEEyNTYgVGltZVN0YW1waW5nIENBMB4XDTIyMDkyMTAw
# MDAwMFoXDTMzMTEyMTIzNTk1OVowRjELMAkGA1UEBhMCVVMxETAPBgNVBAoTCERp
# Z2lDZXJ0MSQwIgYDVQQDExtEaWdpQ2VydCBUaW1lc3RhbXAgMjAyMiAtIDIwggIi
# MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDP7KUmOsap8mu7jcENmtuh6BSF
# dDMaJqzQHFUeHjZtvJJVDGH0nQl3PRWWCC9rZKT9BoMW15GSOBwxApb7crGXOlWv
# M+xhiummKNuQY1y9iVPgOi2Mh0KuJqTku3h4uXoW4VbGwLpkU7sqFudQSLuIaQyI
# xvG+4C99O7HKU41Agx7ny3JJKB5MgB6FVueF7fJhvKo6B332q27lZt3iXPUv7Y3U
# TZWEaOOAy2p50dIQkUYp6z4m8rSMzUy5Zsi7qlA4DeWMlF0ZWr/1e0BubxaompyV
# R4aFeT4MXmaMGgokvpyq0py2909ueMQoP6McD1AGN7oI2TWmtR7aeFgdOej4TJEQ
# ln5N4d3CraV++C0bH+wrRhijGfY59/XBT3EuiQMRoku7mL/6T+R7Nu8GRORV/zbq
# 5Xwx5/PCUsTmFntafqUlc9vAapkhLWPlWfVNL5AfJ7fSqxTlOGaHUQhr+1NDOdBk
# +lbP4PQK5hRtZHi7mP2Uw3Mh8y/CLiDXgazT8QfU4b3ZXUtuMZQpi+ZBpGWUwFjl
# 5S4pkKa3YWT62SBsGFFguqaBDwklU/G/O+mrBw5qBzliGcnWhX8T2Y15z2LF7OF7
# ucxnEweawXjtxojIsG4yeccLWYONxu71LHx7jstkifGxxLjnU15fVdJ9GSlZA076
# XepFcxyEftfO4tQ6dwIDAQABo4IBizCCAYcwDgYDVR0PAQH/BAQDAgeAMAwGA1Ud
# EwEB/wQCMAAwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwIAYDVR0gBBkwFzAIBgZn
# gQwBBAIwCwYJYIZIAYb9bAcBMB8GA1UdIwQYMBaAFLoW2W1NhS9zKXaaL3WMaiCP
# nshvMB0GA1UdDgQWBBRiit7QYfyPMRTtlwvNPSqUFN9SnDBaBgNVHR8EUzBRME+g
# TaBLhklodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRS
# U0E0MDk2U0hBMjU2VGltZVN0YW1waW5nQ0EuY3JsMIGQBggrBgEFBQcBAQSBgzCB
# gDAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMFgGCCsGAQUF
# BzAChkxodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVk
# RzRSU0E0MDk2U0hBMjU2VGltZVN0YW1waW5nQ0EuY3J0MA0GCSqGSIb3DQEBCwUA
# A4ICAQBVqioa80bzeFc3MPx140/WhSPx/PmVOZsl5vdyipjDd9Rk/BX7NsJJUSx4
# iGNVCUY5APxp1MqbKfujP8DJAJsTHbCYidx48s18hc1Tna9i4mFmoxQqRYdKmEIr
# UPwbtZ4IMAn65C3XCYl5+QnmiM59G7hqopvBU2AJ6KO4ndetHxy47JhB8PYOgPvk
# /9+dEKfrALpfSo8aOlK06r8JSRU1NlmaD1TSsht/fl4JrXZUinRtytIFZyt26/+Y
# siaVOBmIRBTlClmia+ciPkQh0j8cwJvtfEiy2JIMkU88ZpSvXQJT657inuTTH4YB
# ZJwAwuladHUNPeF5iL8cAZfJGSOA1zZaX5YWsWMMxkZAO85dNdRZPkOaGK7DycvD
# +5sTX2q1x+DzBcNZ3ydiK95ByVO5/zQQZ/YmMph7/lxClIGUgp2sCovGSxVK05iQ
# RWAzgOAj3vgDpPZFR+XOuANCR+hBNnF3rf2i6Jd0Ti7aHh2MWsgemtXC8MYiqE+b
# vdgcmlHEL5r2X6cnl7qWLoVXwGDneFZ/au/ClZpLEQLIgpzJGgV8unG1TnqZbPTo
# ntRamMifv427GFxD9dAq6OJi7ngE273R+1sKqHB+8JeEeOMIA11HLGOoJTiXAdI/
# Otrl5fbmm9x+LMz/F0xNAKLY1gEOuIvu5uByVYksJxlh9ncBjDCCB2IwggVKoAMC
# AQICEA6ZYkUs3x6FUkpZyx3fNW8wDQYJKoZIhvcNAQELBQAwaTELMAkGA1UEBhMC
# VVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMUEwPwYDVQQDEzhEaWdpQ2VydCBU
# cnVzdGVkIEc0IENvZGUgU2lnbmluZyBSU0E0MDk2IFNIQTM4NCAyMDIxIENBMTAe
# Fw0yMjExMTEwMDAwMDBaFw0yMzExMTAyMzU5NTlaMGcxCzAJBgNVBAYTAkFVMREw
# DwYDVQQIEwhWaWN0b3JpYTEVMBMGA1UEBxMMTm90dGluZyBIaWxsMRYwFAYDVQQK
# Ew1JUFNlYyBQdHkgTHRkMRYwFAYDVQQDEw1JUFNlYyBQdHkgTHRkMIICIjANBgkq
# hkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAuJ4EsEL1x5o2E2m4pFTCeVzGv5URXuSd
# Q25eyXUbMGb7VsTbft4MYM6ySZe7I1Yk7Jm/rsSsjDuud9XbvLJB3qX/kdVCFMLn
# y2On85sFUQ67ZWHGh5ub9tIJzkJN7BlJ0u2Rw09AeLAArF3yqeQffu/YegMSH8rY
# eb8DLvFr9lxKjoWXg5khH3yXWaI0N0g/FHKWL9URn6HSStY90y4ilQCcjwjwA8Vp
# gIU9eCDj4kagwdU5ZvOazbzQXJqvzMB09ccLegf2MrPoMseRSksL0BBo5kkq5jiw
# Fc4HIf2/RYrjfVWtFLq33tvMDifpI/ZuhiBooaAEe3OA10s8+A4Ps3OkSP2hd/Hq
# UXGQZY1FN3fDRworMkEaaqQioeaaSePAIwdGTz2Z2aCY4b5taHWiOM/5+jhCr6Gq
# S8TXI1ODtZl2V11/KfKJDjiTes2lQ4zShHNAg40/aTOQxmsvv1zwlM9Dqf8GRDC1
# OThWrPc40s6RztXa9CnC3EQgxIUQs/qQwkToS3dJGcr/bsCmVCHPQKDjHYIHbU8n
# CMjSaG1YDIfdRW9MyyAFghtzjDDEyyN+LJCOdEnq4WgiaNIHNDsYABTAy9Nc2u4u
# VmQE387ZqbAww2Doo/eiEnPU5y1JgXvxhqgdL2Fg20gXagfBqysTuuExuh/AoSuM
# TEO02MRZ+78CAwEAAaOCAgYwggICMB8GA1UdIwQYMBaAFGg34Ou2O/hfEYb7/mF7
# CIhl9E5CMB0GA1UdDgQWBBTZSP/AY1Bcm0dUbWSS+LJDbsyIHzAOBgNVHQ8BAf8E
# BAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMwgbUGA1UdHwSBrTCBqjBToFGgT4ZN
# aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0Q29kZVNp
# Z25pbmdSU0E0MDk2U0hBMzg0MjAyMUNBMS5jcmwwU6BRoE+GTWh0dHA6Ly9jcmw0
# LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNENvZGVTaWduaW5nUlNBNDA5
# NlNIQTM4NDIwMjFDQTEuY3JsMD4GA1UdIAQ3MDUwMwYGZ4EMAQQBMCkwJwYIKwYB
# BQUHAgEWG2h0dHA6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzCBlAYIKwYBBQUHAQEE
# gYcwgYQwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBcBggr
# BgEFBQcwAoZQaHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1
# c3RlZEc0Q29kZVNpZ25pbmdSU0E0MDk2U0hBMzg0MjAyMUNBMS5jcnQwDAYDVR0T
# AQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAgEAFQZrq6Y5WS74dGqSjbSXHqiQeQhc
# emYWRNkPOnA4duyDdKNerNSkYDhqunW3igVhLkjYzV3rxPceL67+mUdy4ppf9g1Q
# e8f6cM/wELnKmXH/aCGBVvBEjVJnt3LTQLGUGNu28Np1Hdfg7H0SqRlSuvLCaibr
# C87bRHgzCLGixw/Bo2d/SuKze2EXTtt85DLvx7TCOE894bUuhaHUnz207+SUhRnm
# W4pvHel8wJWfDN7QXANFvaV667cOlPZ9Kz2Jrxgf389Pj2XPrfIL/oH3U5OY6Q0/
# dHpFrT8s30MpG5iFQKbYn8W4790Gt/tjJVAFhMxYfnylT+ppemmvrltG/0qah903
# /IxMgSVI9I/23Gs83i7ppeCNnncF/5JRfD2qkQqKRHaJx/6rMjV0MDmV+m16bllZ
# jEfnXIxPyB1P4tJEwiGtbKhuEX9WbdE5msxy64FFU/4Ucxa8dIjllcr1MMfwJttX
# QDLtBYXDFrtb4r26FGRe3M+mjmZ0skzWPEuZpnjR0wK8Ep8HzEdB5WzLB/7lf70n
# YEWfuE1CjviDRz9hesjHZZ7nvPNXuraIRqPdNWnI8eJ7sT+jnL9nP3/h/uDvuHZx
# LAUBvRo6iNOOmO34/TZNCV0fBpZEBUBLJTBmFZoBTMV8z156RL7QXjCK1cLgXuCU
# OfJLfjrNc5RZvtAxggZTMIIGTwIBATB9MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQK
# Ew5EaWdpQ2VydCwgSW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBD
# b2RlIFNpZ25pbmcgUlNBNDA5NiBTSEEzODQgMjAyMSBDQTECEA6ZYkUs3x6FUkpZ
# yx3fNW8wDQYJYIZIAWUDBAIBBQCggYQwGAYKKwYBBAGCNwIBDDEKMAigAoAAoQKA
# ADAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgorBgEEAYI3AgELMQ4wDAYK
# KwYBBAGCNwIBFTAvBgkqhkiG9w0BCQQxIgQg/lenRiva23ONYCnVkb2HMn2pXEIm
# thpD8wgJo/iDbhUwDQYJKoZIhvcNAQEBBQAEggIAn9nncUb/SUmX4pjPBvk8CqOe
# QRRTBDzcIQwxcG6sRemXL3Tdg3cBCsF6WEXtg44ZisLwmMGpq3jErwX0p8kNv6L9
# 9WMU1myEdAXFUlb3xTeUuwm4oqV8+ZQwjcN+92VSRdUOQMzUAe0SkpTSZ1GUCG5Y
# d6yZUYauwaQlHo86VseMU3awdg0B8hsiQu7ZOnXrpt3n41P6/VCMdzQ4+7KnFG9k
# 26Qmw9F/lYgJ3DPPw7p05hMw38uvTsIu314bh3KTIKPB0DVkSuimYyiDUGcmmnp5
# PGAaXM+xXkg0jb3UGbPt/jHRF+AnwQTx7GpGKPvdAq2ZA+Yyh4Y17pHF9x9t1FlD
# x+rK7rqW3LmCo8fg/y/mevjVi5Bbc044BC1/JdzfIJSDxvzksycO/mKo4iwcqCir
# aJNvJnuZZ9YzfOk4T8DGHhuZ40dN91vGsnk4r7PdK9xOPaPDCg94u7Y7xCd43eau
# KlDHCf4LsGjlaaxF4b8FjhSFCEX5YE2VdQw09dNFSCg0Ai9OyK9XRSNF38VAVRLE
# pThjVFrwCtMQDuLSbIjJ9rOky1x2NNle11Q4WF8x0EepqjOul0I3azBaZre0VW7q
# GnnJtlfiGPsO8rU3aeKnuzrIz5XjNKRoHAQk48OhDpFQwCAhDvCdnqEPBsRdUbTa
# kAbHC1B2lrcKJ/nsVVChggMgMIIDHAYJKoZIhvcNAQkGMYIDDTCCAwkCAQEwdzBj
# MQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xOzA5BgNVBAMT
# MkRpZ2lDZXJ0IFRydXN0ZWQgRzQgUlNBNDA5NiBTSEEyNTYgVGltZVN0YW1waW5n
# IENBAhAMTWlyS5T6PCpKPSkHgD1aMA0GCWCGSAFlAwQCAQUAoGkwGAYJKoZIhvcN
# AQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG9w0BCQUxDxcNMjMwNTAyMjMzOTU3WjAv
# BgkqhkiG9w0BCQQxIgQgDq7RcDsSFmgJbVrlNMWueMCxqnbf2sXHnWbSkiyGbgow
# DQYJKoZIhvcNAQEBBQAEggIACmn5OOyMkOrIEcSOfFVU7FBP1I67UhWA8ad6b8Hb
# d/UVhU5yGAv9aAEGER0Rv5UiJFqwHRgagwi5/QyCeyWUvcBbZkNOEiIIg4GYj7Xf
# mgYTJ9ImzYqfs9gmgYyo2fH5+EGc5HI4ymrTKOAHajarx1syaWWDJtOpqHll/u73
# FvTzrx6q87EO3/vR7d/+zKzzPjN+qTU4BWubb0pY/70vO3hyAxi1No/oYWfhYBV3
# KXxUkmY9TcsbAY7tRHbWhHODvQWtQHUK+JDC0sYaa8koaX9wVkb+KMfuZoT/d/8w
# EJes8pIV3xlugKsW3DlXHB9FPKHtYVUbZ11SZ5KZm5TOJwPxTm5GlqV6+bBZtSoI
# QLfk42YJ9AJiwSbpicYkcfVTf25N0w+2cjZkbOwoOgPakR2PJKxhhb2RLDA2Aqs+
# VJaOgBBSTlUIYyTDnOwxta5bMfFPNrwTehfOhAZJS7Pypaq5IK9e6SDcp/kh0ShL
# ADLo522d2qFh8Zc/QbC5FxSM0KBOcGHlAd48lrnceN78h26uCgBuefW5Y0iWOvlV
# lJqZXs0VcDi1zgAT5Yq/Q9wrKVPECzHuDV9AhcJWufZcMWH4jhWB7r2AnkGk3NFw
# c3k+dDziqlWxYYf1lQ8UGJ0d8bvWm3ufikoqJtR95vWdKFLT3GvtxuvpUke1wyIn
# hIQ=
# SIG # End signature block