HP.Repo.psm1

#
# Copyright 2018-2021 HP Development Company, L.P.
# All Rights Reserved.
#
# NOTICE: All information contained herein is, and remains the property of HP Development Company, L.P.
#
# The intellectual and technical concepts contained herein are proprietary to HP Development Company, L.P
# and may be covered by U.S. and Foreign Patents, patents in process, and are protected by
# trade secret or copyright law. Dissemination of this information or reproduction of this material
# is strictly forbidden unless prior written permission is obtained from HP Development Company, L.P.

Set-StrictMode -Version 5.1
#requires -Modules "HP.Private","HP.Softpaq"

$RepositoryType = @"
public enum ErrorHandling {
  Fail = 0,
  LogAndContinue = 1
};
 
public class SoftpaqRepositoryFile {
    public class SoftpaqRepositoryFilter {
            public string platform;
            public string operatingSystem;
            public string category;
            public string releaseType;
            public string characteristic;
    };
 
    public class NotificationConfiguration {
            public string server;
            public int port;
            public bool tls;
            public string[] addresses;
            public string username;
            public string password;
            public string from;
            public string fromname;
    };
     
    public class Configuration {
        public ErrorHandling OnRemoteFileNotFound;
    public int ExclusiveLockMaxRetries;
    public string OfflineCacheMode;
    public string RepositoryReport;
        }
 
 
    public string DateCreated;
    public string DateLastModified;
    public string CreatedBy;
    public string ModifiedBy;
    public SoftpaqRepositoryFilter[] Filters;
    public NotificationConfiguration Notifications;
    public Configuration Settings;
 
 
}
"@


$REPOFILE = ".repository/repository.json"
$LOGFILE = ".repository/activity.log"

Add-Type -TypeDefinition $RepositoryType

# print a bare error
function err
{
  [CmdletBinding()]
  param(
    [string]$str,
    [boolean]$withLog = $true
  )

  [console]::ForegroundColor = 'red'
  [console]::Error.WriteLine($str)
  [console]::ResetColor()

  if ($withLog) { Write-LogError -Message $str -Component "HP.Repo" -File $LOGFILE }
}

# convert a date object to an 8601 string
function ISO8601DateString 
{
  [CmdletBinding()]
  param(
    [DateTime]$Date
    )
  $Date.ToString("yyyy-MM-dd'T'HH:mm:ss.fffffff",[System.Globalization.CultureInfo]::InvariantCulture)
}

# get current user name
function GetUserName ()
{
  [CmdletBinding()]
  param()

  try {
    [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
  }
  catch {
    return $env:username
  }
}

# check if a file exists
function FileExists
{
  [CmdletBinding()]
  param(
    [string]$File
    )
  Test-Path $File -PathType Leaf
}

# load a json object
function LoadJson 
{
  [CmdletBinding()]
  param(
    [string]$File
  )

  try {
    $PS7Mark = "PS7Mark"
    $rawData = (Get-Content -Raw -Path $File) -replace '("DateLastModified": ")([^"]+)(")', ('$1' + $PS7Mark + '$2' + $PS7Mark + '$3')
    [SoftpaqRepositoryFile]$result = $rawData | ConvertFrom-Json
    $result.DateLastModified = $result.DateLastModified -replace $PS7Mark, ""
    return $result
  }
  catch
  {
    err ("Could not parse '$File' $($_.Exception.Message)")
    return $Null
  }
}

# load a repository definition file
function LoadRepository 
{
  [CmdletBinding()]
  param()

  Write-Verbose "loading $REPOFILE"
  $inRepo = FileExists -File $REPOFILE
  if (-not $inRepo) {
    throw [System.Management.Automation.ItemNotFoundException]"Directory '$(Get-Location)' is not a repository."
  }

  $repo = LoadJson -File $REPOFILE
  if (-not $repo -eq $null)
  {
    err ("Could not initialize the repository: $($_.Exception.Message)")
    return $false,$null
  }

  if (-not $repo.Filters) { $repo.Filters = @() }

  if (-not $repo.Settings) {
    $repo.Settings = New-Object SoftpaqRepositoryFile+Configuration
  }

  if (-not $repo.Settings.OnRemoteFileNotFound) {
    $repo.Settings.OnRemoteFileNotFound = [ErrorHandling]::Fail
  }

  if (-not $repo.Settings.ExclusiveLockMaxRetries) {
    $repo.Settings.ExclusiveLockMaxRetries = 10
  }

  if (-not $repo.Settings.OfflineCacheMode) {
    $repo.Settings.OfflineCacheMode = "Disable"
  }

  if (-not $repo.Settings.RepositoryReport) {
    $repo.Settings.RepositoryReport = "CSV"
  }

  foreach ($filter in $repo.Filters)
  {
    if (-not $filter.characteristic )
    {
      $filter.characteristic = "*"
    }
  }

  if (-not $repo.Notifications ) {
    $repo.Notifications = New-Object SoftpaqRepositoryFile+NotificationConfiguration
    $repo.Notifications.port = 25
    $repo.Notifications.tls = $false
    $repo.Notifications.username = ""
    $repo.Notifications.password = ""
    $repo.Notifications.from = "softpaq-repo-sync@$($env:userdnsdomain)"
    $repo.Notifications.fromname = "Softpaq Repository Notification"
  }

  Write-Verbose "load success"
  return $true,$repo
}

# download a softpaq, optionally checking existing softpaqs. Note that CVAs are always
# downloaded since there is no reliable way to check their consistency.

function DownloadSoftpaq 
{
  [CmdletBinding()]
  param(
    $DownloadSoftpaqCmd,
    [int]$maxRetries = 10
  )
  $download_file = $true
  $filename = "sp" + $DownloadSoftpaqCmd.number + ".exe"
  $CVAname = "sp" + $DownloadSoftpaqCmd.number + ".cva"

  # downloading the CVA
  Write-Verbose ("Downloading CVA $($DownloadSoftpaqCmd.number)")
  Log (" sp$($DownloadSoftpaqCmd.number).cva - Downloading CVA file.")
  Get-SoftpaqMetadataFile @DownloadSoftpaqCmd -MaxRetries $maxRetries
  Log (" sp$($DownloadSoftpaqCmd.number).cva - Done downloading CVA file.")

  if (FileExists -File $filename) {
    Write-Verbose "Checking signature for existing file $filename"
    if (Get-HPPrivateCheckSignature -File $filename -CVAfile $CVAname -Verbose:$VerbosePreference -progress:(-not $DownloadSoftpaqCmd.Quiet)) {

      if (-not $DownloadSoftpaqCmd.Quiet) {
        Write-Host -ForegroundColor Magenta "File $filename already exists and passes signature check. Will not redownload."
      }
      Log (" sp$($DownloadSoftpaqCmd.number).exe - Already exists. Will not redownload.")
      $download_file = $false
    }
    else {
      Write-Verbose ("Need to redownload file '$filename'")
    }
  }
  else {
    Write-Verbose ("Need to download file '$filename'")
  }


  if ($download_file -eq $true) {
    try {
      Log (" sp$($DownloadSoftpaqCmd.number).exe - Downloading EXE file.")
      Get-Softpaq @DownloadSoftpaqCmd -MaxRetries $maxRetries -Overwrite yes
      # check post-download integrity
      if (-not (Get-HPPrivateCheckSignature -File $filename -CVAfile $CVAname -Verbose:$VerbosePreference -progress:(-not $DownloadSoftpaqCmd.Quiet))) {
            Remove-Item -Path $filename -Force -Verbose:$VerbosePreference
            Remove-Item -Path $CVAName -Force -Verbose:$VerbosePreference
            $msg = "File $filename failed integrity check and has been deleted, will retry download next sync"
            if (-not $DownloadSoftpaqCmd.Quiet) {
                Write-Host -ForegroundColor Magenta $msg               
             }
             Write-LogWarning  -Message $msg -Component "HP.Repo" -File $LOGFILE

          }
      Log (" sp$($DownloadSoftpaqCmd.number).exe - Done downloading EXE file.")
    }
    catch {
      Write-Host -ForegroundColor Magenta "File sp$($DownloadSoftpaqCmd.number).exe has invalid or missing signature and will be deleted."
      Log (" sp$($DownloadSoftpaqCmd.number).exe has invalid or missing signature and will be deleted.")
      Log (" sp$($DownloadSoftpaqCmd.number).exe - Redownloading EXE file.")
      Get-Softpaq @DownloadSoftpaqCmd -MaxRetries $maxRetries
      Log (" sp$($DownloadSoftpaqCmd.number).exe - Done downloading EXE file.")
    }
  }
}

# write a repository definition file
function WriteRepositoryFile 
{
  [CmdletBinding()]
  param($obj)

  $now = Get-Date
  $obj.DateLastModified = ISO8601DateString -Date $now
  $obj.ModifiedBy = GetUserName
  Write-Verbose "Writing repository file to $REPOFILE"
  $obj | ConvertTo-Json | Out-File -Force $REPOFILE
}

# check if a filter exists in a repo object
function FilterExists 
{
  [CmdletBinding()]
  param($repo, $f)

  $c = getFilters $repo $f
  return ($null -ne $c)
}

# get a list of filters in a repo, matching exact parameters
function GetFilters 
{
  [CmdletBinding()]
  param($repo, $f)

  if ($repo.Filters.Count -eq 0) { return $null }
  $repo.Filters | Where-Object {
    $_.platform -eq $f.platform -and
    $_.operatingSystem -eq $f.operatingSystem -and
    $_.category -eq $f.category -and
    $_.releaseType -eq $f.releaseType -and
    $_.characteristic -eq $f.characteristic
  }
}

# get a list of filters in a repo, considering empty parameters as wildcards
function GetFiltersWild 
{
  [CmdletBinding()]
  param($repo, $f)

  if ($repo.Filters.Count -eq 0) { return $null }
  $repo.Filters | Where-Object {
    $_.platform -eq $f.platform -and
    (
      $_.operatingSystem -eq $f.operatingSystem -or
      $f.operatingSystem -eq "*" -or
      ($f.operatingSystem -eq "win10:*" -and $_.operatingSystem.startsWith("win10"))
    ) -and
    ($_.category -eq $f.category -or $f.category -eq "*") -and
    ($_.releaseType -eq $f.releaseType -or $f.releaseType -eq "*") -and
    ($_.characteristic -eq $f.characteristic -or $f.characteristic -eq "*")
  }
}

# write a log entry to the .repository/activity.log
function Log
{
  [CmdletBinding()]
  param([string[]]$entryText)

  foreach ($line in $entryText)
  {
    if (-not $line) {
      $line = " "
    }
    Write-LogInfo -Message $line -Component "HP.Repo" -File $LOGFILE
  }

}

# touch a file (change its date if exists, or create it if it doesn't.
function TouchFile 
{
  [CmdletBinding()]
  param([string] $File)

  if (Test-Path $File) { (Get-ChildItem $File).LastWriteTime = Get-Date }
  else { Write-Output $null > $File }
}


# remove all marks from the repository
function FlushMarks
{
  [CmdletBinding()]
  param()

  Write-Verbose "Removing all marks"
  Remove-Item ".repository\mark\*" -Include "*.mark"
}


# send a notification email
function Send
{
  [CmdletBinding()]
  param(
    $subject,
    $body,
    $html = $true
  )

  $n = Get-RepositoryNotificationConfiguration
  if ((-not $n) -or (-not $n.server)) {
    Write-Verbose ("Notifications are not configured")
    return
  }

  try {
    if ((-not $n.addresses) -or (-not $n.addresses.Count))
    {
      Write-Verbose ("Notifications has no recipients defined")
      return
    }
    log ("Sending a notification email")

    $params = @{}
    $params.To = $n.addresses
    $params.SmtpServer = $n.server
    $params.port = $n.port
    $params.UseSsl = $n.tls
    $params.from = "$($n.fromname) <$($n.from)>"
    $params.Subject = $subject
    $params.Body = $body
    $params.BodyAsHtml = $html

    Write-Verbose ("server: $($params.SmtpServer)")
    Write-Verbose ("port: $($params.Port)")

    if ([string]::IsNullOrEmpty($n.username) -eq $false)
    {
      try {
        [SecureString]$read = ConvertTo-SecureString -string $n.password -AsPlainText -Force
        $params.Credential = New-Object System.Management.Automation.PSCredential ($n.username,$read)
        if (-not $params.Credential) {
          log ("Could not build credential object from username and password")
          return;
        }
      }
      catch {
        err ("Failed to build credential object from username and password: $($_.Exception.Message)")
        return
      }
    }
    Send-MailMessage @params -ErrorAction Stop
  }
  catch {
    err ("Could not send email: $($_.Exception.Message)")
    return
  }
  Write-Verbose ("Send complete.")
}

<#
.SYNOPSIS
    Initialize a repository in the current directory.
 
.DESCRIPTION
    This command initializes a directory to be used as a repository. It creates a .repository folder in the current directory,
    which contains the definition of the .repository and all its settings.
 
    In order to un-initalize a directory, simple remove the .repository folder.
 
    After initializing a repository, you must add at least one filter to define the content that this repository will receive.
 
    If the directory already contains a repository, the command will fail.
 
.EXAMPLE
    Initialize-Repository
 
.LINK
    [Add-RepositoryFilter](Add-RepositoryFilter)
 
.LINK
    [Remove-RepositoryFilter](Remove-RepositoryFilter)
 
.LINK
    [Get-RepositoryInfo](Get-RepositoryInfo)
 
.LINK
    [Invoke-RepositorySync](Invoke-RepositorySync)
 
.LINK
    [Invoke-RepositoryCleanup](Invoke-RepositoryCleanup)
 
.LINK
    [Set-RepositoryNotificationConfiguration](Set-RepositoryNotificationConfiguration)
 
.LINK
    [Clear-RepositoryNotificationConfiguration](Clear-RepositoryNotificationConfiguration)
 
.LINK
    [Get-RepositoryNotificationConfiguration](Get-RepositoryNotificationConfiguration)
 
.LINK
    [Show-RepositoryNotificationConfiguration](Show-RepositoryNotificationConfiguration)
 
.LINK
    [Add-RepositorySyncFailureRecipient](Add-RepositorySyncFailureRecipient)
 
.LINK
    [Remove-RepositorySyncFailureRecipient](Remove-RepositorySyncFailureRecipient)
 
.LINK
    [Test-RepositoryNotificationConfiguration](Test-RepositoryNotificationConfiguration)
 
.LINK
    [Get-RepositoryConfiguration](Get-RepositoryConfiguration)
 
.LINK
    [Set-RepositoryConfiguration](Set-RepositoryConfiguration)
#>

function Initialize-Repository
{
  [CmdletBinding(HelpUri = "https://developers.hp.com/hp-client-management/doc/Initialize%E2%80%90Repository")]
  param()

  if (FileExists -File $REPOFILE) {
    err "This directory is already initialized as a repository."
    return
  }
  $now = Get-Date
  $newRepositoryFile = New-Object SoftpaqRepositoryFile

  $newRepositoryFile.Settings = New-Object SoftpaqRepositoryFile+Configuration
  $newRepositoryFile.Settings.OnRemoteFileNotFound = [ErrorHandling]::Fail
  $newRepositoryFile.Settings.ExclusiveLockMaxRetries = 10
  $newRepositoryFile.Settings.OfflineCacheMode = "Disable"
  $newRepositoryFile.Settings.RepositoryReport = "CSV"

  $newRepositoryFile.DateCreated = ISO8601DateString -Date $now
  $newRepositoryFile.CreatedBy = GetUserName

  try {
    New-Item -ItemType directory -Path .repository | Out-Null
    WriteRepositoryFile -obj $newRepositoryFile
    New-Item -ItemType directory -Path ".repository/mark" | Out-Null
  }
  catch {
    err ("Could not initialize the repository: $($_.Exception.Message)")
    return
  }
  Log "Repository initialized successfully."
}

<#
.SYNOPSIS
    Add a filter to the repository.
 
.DESCRIPTION
    This function adds a filter to a repository, which was previously initialized by Initialize-Repository.
 
    The repository can contain one or more filters, and the effective filtering will be the sum of all filters defined.
 
.PARAMETER Platform
   Specifies the given platform as a platform to include in this repository. This is a platform ID, a 4-digit hexadecimal number, as obtained by Get?HPDeviceProductID.
 
.PARAMETER Os
    Specifies the operating system to be include in this repository. The field must be one of "win7", "win8", "win8.1", "win10". If this parameter is not specified, all operating system associated with the specified platform will be included.
 
.PARAMETER OsVer
    For windows 10 only, specify the target OS Version (e.g. 1709, 1803, 2009 etc). Starting from 21H1 release, "xxHx" format is expected. If the parameter is not specified, current operating system version
    will be assumed, which may not be what is intended.
 
    For windows versions other than windows 10, this switch is silently ignored.
 
.PARAMETER Category
    Specifies the softpaq category to be include in this repository. The category must be one (or more) of "Bios", "Firmware", "Driver", "Software", "OS", "Manageability", "Diagnostic", "Utility", "Driverpack", "Dock", "UWPPack".
    If this parameter is not specified, all categories are included.
 
.PARAMETER ReleaseType
    Specifies the softpaq release type to be include in this repository. The release type must be one (or more) of "critical", "recommended", "routine". If this parameter is not specified, all release types are included.
 
.PARAMETER Characteristic
    Specifies the softpaq characteristic to be include in this repository. The characteristic must be one of "ssm", "dpb", "uwp". If this parameter is not specified, all characteristics are included.
 
.EXAMPLE
    Add-RepositoryFilter -Platform 1234 -Os win10 -OsVer 2009
 
.EXAMPLE
    Add-RepositoryFilter -Platform 1234 -Os win10 -OsVer "21H1"
 
.LINK
    [Initialize-Repository](Initialize-Repository)
 
.LINK
    [Remove-RepositoryFilter](Remove-RepositoryFilter)
 
.LINK
    [Get-RepositoryInfo](Get-RepositoryInfo)
 
.LINK
    [Invoke-RepositoryCleanup](Invoke-RepositoryCleanup)
 
.LINK
    [Invoke-RepositorySync](Invoke-RepositorySync)
 
.LINK
    [Set-RepositoryNotificationConfiguration](Set-RepositoryNotificationConfiguration)
 
.LINK
    [Clear-RepositoryNotificationConfiguration](Clear-RepositoryNotificationConfiguration)
 
.LINK
    [Get-RepositoryNotificationConfiguration](Get-RepositoryNotificationConfiguration)
 
.LINK
    [Show-RepositoryNotificationConfiguration](Show-RepositoryNotificationConfiguration)
 
.LINK
    [Add-RepositorySyncFailureRecipient](Add-RepositorySyncFailureRecipient)
 
.LINK
    [Remove-RepositorySyncFailureRecipient](Remove-RepositorySyncFailureRecipient)
 
.LINK
    [Test-RepositoryNotificationConfiguration](Test-RepositoryNotificationConfiguration)
 
.LINK
    [Get-HPDeviceProductID](Get-HPDeviceProductID)
#>

function Add-RepositoryFilter
{
  [CmdletBinding(HelpUri = "https://developers.hp.com/hp-client-management/doc/Add%E2%80%90RepositoryFilter")]
  param(
    [ValidatePattern("^[a-fA-F0-9]{4}$")]
    [Parameter(Position = 0,Mandatory = $true)] [string]$Platform,

    [ValidateSet("win7","win8","win8.1","win81","win10","*")] # keep in sync with the softpaq module
    [string[]]
    [Parameter(Position = 1)] $Os = "*",

    [Parameter(Position = 1)] [string]$OsVer,

    [ValidateSet("Bios","Firmware","Driver","Software","Os","Manageability","Diagnostic","Utility","Driverpack","Dock","UWPPack","*")] # keep in sync with the softpaq module
    [string[]]
    [Parameter(Position = 2)] $Category = "*",

    [ValidateSet("Critical","Recommended","Routine","*")] # keep in sync with the softpaq module
    [string[]]
    [Parameter(Position = 3)] $ReleaseType = "*",

    [ValidateSet("SSM","DPB","UWP","*")] # keep in sync with the softpaq module
    [string[]]
    [Parameter(Position = 4)] $Characteristic = "*"
  )

  $c = LoadRepository
  try {
    if ($c[0] -eq $false) { return }
    $repo = $c[1]

    $newFilter = New-Object SoftpaqRepositoryFile+SoftpaqRepositoryFilter
    $newFilter.platform = $Platform

    $newFilter.operatingSystem = $Os
    if (-not $OsVer)
    {
      $OsVer = GetCurrentOSVer
    }
    if ($OsVer) { $OsVer = $OsVer.ToLower() }
    if ($Os -eq "win10") { $newFilter.operatingSystem = "win10:$OsVer" }

    $newFilter.category = $Category
    $newFilter.releaseType = $ReleaseType
    $newFilter.characteristic = $Characteristic

    # silently ignore if the filter is already in the repo
    $exists = filterExists $repo $newFilter
    if (!$exists) {
      $repo.Filters += $newFilter
      WriteRepositoryFile -obj $repo
      if($OsVer -and $Os -ne '*') { Log "Added filter $Platform {{ os='$Os', osver='$OsVer', category='$Category', release='$ReleaseType', characteristic='$Characteristic' }}" }
      else {Log "Added filter $Platform {{ os='$Os', category='$Category', release='$ReleaseType', characteristic='$Characteristic' }}"}
    }
    else
    {
      Write-Verbose ("Silently ignoring this filter, exact match is already in repository")
    }
    Write-Verbose "Repository Filter Added."
  }
  catch
  {
    err ("Could not add filter to repository: $($_.Exception.Message)")
  }
}


<#
.SYNOPSIS
    Remove one or more filters from the repository.
     
.DESCRIPTION
    This function modifies the repository to remove filters from the repository definition.
 
    If an optional parameter is not specified, it will be considered a wildcard and match any value. Therefore this command may
    result in multiple filters being deleted.
 
.PARAMETER Platform
    The platform to remove. This is a 4-digit hex number, and can be obtained via Get-HPDeviceProductID
 
.PARAMETER Os
    An optional parameter to narrow down the filter to a specific OS for the specified platform. If not specified, all OS will be matched.
 
.PARAMETER OsVer
    For windows 10 only, specify the target OS Version (e.g. 1709, 1803, 2009 etc). If the parameter is not specified all windows 10 filters will match. Starting from 21H1 release, "xxHx" format is expected.
 
    For windows versions other than windows 10, this switch is silently ignored.
 
.PARAMETER Category
    An optional parameter to narrow down the filter to a specific category for the specified platform. If not specified, all categories will be matched.
 
.PARAMETER ReleaseType
    An optional parameter to narrow down the filter to a specific release type for the specified platform. If not specified, all release types will be matched.
 
.PARAMETER Characteristic
    An optional parameter to narrow down the filter to a specific characteristic for the specified platform. If not specified, all characteristics will be matched.
 
.PARAMETER Yes
    An optional parameter to provide. If not specified, script will ask for confirmation before deleting a filter. If specified, script will go ahead and delete the filter without confirming.
 
.EXAMPLE
    Remove-RepositoryFilter -Platform 1234
 
.EXAMPLE
    Remove-RepositoryFilter -Platform 1234 -Os win10 -OsVer "21H1"
     
.LINK
    [Initialize-Repository](Initialize-Repository)
 
.LINK
    [Add-RepositoryFilter](Add-RepositoryFilter)
 
.LINK
    [Get-RepositoryInfo](Get-RepositoryInfo)
 
.LINK
    [Invoke-RepositoryCleanup](Invoke-RepositoryCleanup)
 
.LINK
    [Invoke-RepositorySync](Invoke-RepositorySync)
 
.LINK
    [Set-RepositoryNotificationConfiguration](Set-RepositoryNotificationConfiguration)
 
.LINK
    [Clear-RepositoryNotificationConfiguration](Clear-RepositoryNotificationConfiguration)
 
.LINK
    [Get-RepositoryNotificationConfiguration](Get-RepositoryNotificationConfiguration)
 
.LINK
    [Show-RepositoryNotificationConfiguration](Show-RepositoryNotificationConfiguration)
 
.LINK
    [Add-RepositorySyncFailureRecipient](Add-RepositorySyncFailureRecipient)
 
.LINK
    [Remove-RepositorySyncFailureRecipient](Remove-RepositorySyncFailureRecipient)
 
.LINK
    [Get-HPDeviceProductID](Get-HPDeviceProductID)
 
.LINK
    [Test-RepositoryNotificationConfiguration](Test-RepositoryNotificationConfiguration)
#>

function Remove-RepositoryFilter
{
  [CmdletBinding(HelpUri = "https://developers.hp.com/hp-client-management/doc/Remove%E2%80%90RepositoryFilter")]
  param(
    [ValidatePattern("^[a-fA-F0-9]{4}$")]
    [Parameter(Position = 0,Mandatory = $true)] 
    [string]$Platform,

    [ValidateSet("win7","win8","win8.1","win81","win10","*")] # keep in sync with the softpaq module
    [string[]]
    [Parameter(Position = 1)] 
    $Os = "*",

    [Parameter(Position = 1)] 
    [string]$OsVer,

    [ValidateSet("Bios","Firmware","Driver","Software","Os","Manageability","Diagnostic","Utility","Driverpack","Dock","UWPPack","*")] # keep in sync with the softpaq module
    [string[]]
    [Parameter(Position = 2)] 
    $Category = "*",

    [ValidateSet("Critical","Recommended","Routine","*")] # keep in sync with the softpaq module
    [string[]]
    [Parameter(Position = 3)] 
    $ReleaseType = "*",

    [Parameter(Position = 4,Mandatory = $false)] 
    [switch]$Yes = $false,

    [ValidateSet("SSM","DPB","UWP","*")] # keep in sync with the softpaq module
    [string[]]
    [Parameter(Position = 5)] 
    $Characteristic = "*"
  )

  $c = LoadRepository
  try {
    if ($c[0] -eq $false) { return }

    $newFilter = New-Object SoftpaqRepositoryFile+SoftpaqRepositoryFilter
    $newFilter.platform = $Platform
    $newFilter.operatingSystem = $Os

    if ($Os -eq "win10") {
      if ($OsVer) { $newFilter.operatingSystem = "win10:$OsVer" }
      else { $newFilter.operatingSystem = "win10:*" }
    }

    $newFilter.category = $Category
    $newFilter.releaseType = $ReleaseType
    $newFilter.characteristic = $Characteristic

    $todelete = getFiltersWild $c[1] $newFilter
    if (-not $todelete) {
      Write-Verbose ("No matching filter to delete")
      return
    }

    if (-not $Yes.IsPresent) {
      Write-Host "The following filters will be deleted:" -ForegroundColor Cyan
      $todelete | ConvertTo-Json -Depth 2 | Write-Host -ForegroundColor Cyan
      $answer = Read-Host "Enter 'y' to continue: "
      if ($answer -ne "y") {
        Write-Host 'Aborted.'
        return }
    }

    $c[1].Filters = $c[1].Filters | Where-Object { $todelete -notcontains $_ }
    WriteRepositoryFile -obj $c[1]

    foreach ($f in $todelete) {
      Log "Removed filter $($f.platform) { os='$($f.operatingSystem)', category='$($f.category)', release='$($f.releaseType), characteristic='$($f.characteristic)' }"
    }
  }
  catch
  {
    err ("Could not remove filter from repository: $($_.Exception.Message)")
  }
}

<#
.SYNOPSIS
    Show the current repository definition.
 
.DESCRIPTION
    Get the repository definition as an object. This command must be run inside an initialized repository.
     
.EXAMPLE
    $myrepository = Get-RepositoryInfo
         
.LINK
    [Initialize-Repository](Initialize-Repository)
 
.LINK
    [Add-RepositoryFilter](Add-RepositoryFilter)
 
.LINK
    [Remove-RepositoryFilter](Remove-RepositoryFilter)
 
.LINK
    [Invoke-RepositorySync](Invoke-RepositorySync)
 
.LINK
    [Invoke-RepositoryCleanup](Invoke-RepositoryCleanup)
 
.LINK
    [Set-RepositoryNotificationConfiguration](Set-RepositoryNotificationConfiguration)
 
.LINK
    [Clear-RepositoryNotificationConfiguration](Clear-RepositoryNotificationConfiguration)
 
.LINK
    [Get-RepositoryNotificationConfiguration](Get-RepositoryNotificationConfiguration)
 
.LINK
    [Show-RepositoryNotificationConfiguration](Show-RepositoryNotificationConfiguration)
 
.LINK
    [Add-RepositorySyncFailureRecipient](Add-RepositorySyncFailureRecipient)
 
.LINK
    [Remove-RepositorySyncFailureRecipient](Remove-RepositorySyncFailureRecipient)
 
.LINK
    [Test-RepositoryNotificationConfiguration](Test-RepositoryNotificationConfiguration)
#>

function Get-RepositoryInfo ()
{
  [CmdletBinding(HelpUri = "https://developers.hp.com/hp-client-management/doc/Get%E2%80%90RepositoryInfo")]
  param()

  $c = LoadRepository
  try {
    if (-not $c[0]) { return }
    $c[1]
  }
  catch
  {
    err ("Could not get repository info: $($_.Exception.Message)")
  }
}

<#
.SYNOPSIS
    Synchronize repository and generate a report with repository content information
     
.DESCRIPTION
    This command performs a synchronization of a repository, by downloading latest SoftPaqs associated with
    the repository filters. At the end of each sync, it create a repository report in a format (default .CSV) set via Set-RepositoryConfiguration.
 
    The command may be scheduled via task manager to run on schedule. Define a notification email via Set-RepositoryNotificationConfiguration
    to receive any failure notifications during unattended operation.
 
This command may be followed by Invoke-RepositoryCleanup to remove any obsolete SoftPaqs from the repository.
   
Invoke-RepositorySync functionality is not supported in WinPE.
 
.PARAMETER Quiet
    Suppress progress messages during operation.
 
.PARAMETER Overwrite
    This parameter controls the overwrite behavior. Options may be "no" to not overwrite existing files,
    "yes" to force overwrite, and "skip" to skip existing files without an error. Default is 'skip' if overwrite is
    not specified.
 
    Note that specifying 'no' is of very limited use for this particular cmdlet, and normally not a good choice.
 
.EXAMPLE
    Invoke-RepositorySync -Quiet
 
.LINK
    [Initialize-Repository](Initialize-Repository)
 
.LINK
    [Add-RepositoryFilter](Add-RepositoryFilter)
 
.LINK
    [Remove-RepositoryFilter](Remove-RepositoryFilter)
 
.LINK
    [Get-RepositoryInfo](Get-RepositoryInfo)
 
.LINK
    [Invoke-RepositoryCleanup](Invoke-RepositoryCleanup)
 
.LINK
    [Set-RepositoryNotificationConfiguration](Set-RepositoryNotificationConfiguration)
 
.LINK
    [Clear-RepositoryNotificationConfiguration](Clear-RepositoryNotificationConfiguration)
 
.LINK
    [Get-RepositoryNotificationConfiguration](Get-RepositoryNotificationConfiguration)
 
.LINK
    [Show-RepositoryNotificationConfiguration](Show-RepositoryNotificationConfiguration)
 
.LINK
    [Add-RepositorySyncFailureRecipient](Add-RepositorySyncFailureRecipient)
 
.LINK
    [Remove-RepositorySyncFailureRecipient](Remove-RepositorySyncFailureRecipient)
 
.LINK
    [Test-RepositoryNotificationConfiguration](Test-RepositoryNotificationConfiguration)
#>

function Invoke-RepositorySync
{
  [CmdletBinding(HelpUri = "https://developers.hp.com/hp-client-management/doc/Invoke%E2%80%90RepositorySync")]
  param(
    [Parameter(Position = 0,Mandatory = $false)] [switch]$Quiet = $false
  )

  $repo = LoadRepository
  try {
    $cwd = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath((Get-Location))
    $cacheDir = Join-Path -Path $cwd -ChildPath ".repository"
    $cacheDirOffline = $cacheDir + "\cache\offline"
    $reportDir = $cacheDir

    # return if repository is not initialized
    if ($repo[0] -eq $false) { return }

    # return if repository is initialized but no filters added
    $filters = $repo[1].Filters
    if ($filters.Count -eq 0) {
      Write-Verbose "Repository has no filters defined, terminating."
      return
    }

    $platformGroups = $filters | Group-Object -Property platform
    $normalized = @()

    foreach ($pobj in $platformGroups)
    {

      $items = $pobj.Group

      if ($items | Where-Object -Property operatingSystem -EQ -Value "*") {
        $items | ForEach-Object { $_.operatingSystem = "*" }
      }

      if ($items | Where-Object -Property category -EQ -Value "*") {
        $items | ForEach-Object { $_.category = "*" }
      }

      if ($items | Where-Object -Property releaseType -EQ -Value "*") {
        $items | ForEach-Object { $_.releaseType = "*" }
      }

      if ($items | Where-Object -Property characteristic -EQ -Value "*") {
        $items | ForEach-Object { $_.characteristic = "*" }
      }

      $normalized += $items | sort -Unique -Property operatingSystem,category,releaseType,characteristic
    }

    $softpaqlist = @()
    Log "Repository sync has started"
    $softpaqListCmd = @{}


    # build the list of SoftPaqs to download
    foreach ($c in $normalized) {
      Write-Verbose ($c | Format-List | Out-String)

      if (Get-HPDeviceDetails -Platform $c.platform)
      {
        $softpaqListCmd.platform = $c.platform.ToLower()
        $softpaqListCmd.Quiet = $Quiet
        $softpaqListCmd.verbose = $VerbosePreference

        Write-Verbose ("Working on a rule for platform $($softpaqListCmd.platform)")

        if ($c.operatingSystem.startsWith("win10:"))
        {
          $split = $c.operatingSystem -split ':'
          $softpaqListCmd.os = $split[0]
          $softpaqListCmd.osver = $split[1]
        }
        elseif ($c.operatingSystem -eq "win10")
        {
          $softpaqListCmd.os = "win10"
          $softpaqListCmd.osver = GetCurrentOSVer
        }
        elseif ($c.operatingSystem -ne "*")
        {
          $softpaqListCmd.os = $c.operatingSystem
          #$softpaqListCmd.osver = $null
        }

        if ($c.characteristic -ne "*")
        {
          $softpaqListCmd.characteristic = $c.characteristic.ToUpper().Split()
          Write-Verbose "Filter-characteristic:$($softpaqListCmd.characteristic)"
        }

        if ($c.releaseType -ne "*")
        {
          $softpaqListCmd.releaseType = $c.releaseType.Split()
          Write-Verbose "Filter-releaseType:$($softpaqListCmd.releaseType)"
        }
        if ($c.category -ne "*")
        {
          $softpaqListCmd.category = $c.category.Split()
          Write-Verbose "Filter-category:$($softpaqListCmd.category)"
        }

        Log "Reading the softpaq list for platform $($softpaqListCmd.platform)"
        $results = Get-SoftpaqList @softpaqListCmd -cacheDir $cacheDir -MaxRetries $repo[1].Settings.ExclusiveLockMaxRetries
        Log "softpaq list for platform $($softpaqListCmd.platform) created"
        $softpaqlist += $results


        $OfflineCacheMode = $repo[1].Settings.OfflineCacheMode
        if ($OfflineCacheMode -eq "Enable") {

          # keep the download order of PlatformList, Advisory data and Knowledge Base as is to maintain unit tests
          $baseurl = "https://hpia.hpcloud.hp.com/ref/"
          $url = $baseurl + "platformList.cab"
          $filename = "platformList.cab"
          Write-Verbose "Trying to download PlatformList..."
          try {
            $PlatformList = Get-HPPrivateOfflineCacheFiles -url $url -FileName $filename -cacheDirOffline $cacheDirOffline -Expand
            Write-Verbose "Finish downloading PlatformList - $PlatformList"
          }
          catch {
            Write-Verbose "Trying to download PlatformList from FTP..."
            $url = "https://ftp.hp.com/pub/caps-softpaq/cmit/imagepal/ref/platformList.cab"
            $PlatformList = Get-HPPrivateOfflineCacheFiles -url $url -FileName $filename -cacheDirOffline $cacheDirOffline -Expand
            if (-not $PlatformList) {
              $exception = $_.Exception
              switch ($repo[1].Settings.OnRemoteFileNotFound) {
                "LogAndContinue" {
                  [string]$data = formatSyncErrorMessageAsHtml $exception
                  Log ($data -split "`n")
                  send "Softpaq repository synchronization error" $data
                }
                # "Fail"
                default {
                  throw $exception
                }
              }
            }
          }

          # download Advisory data
          $url = $baseurl + "$($softpaqListCmd.platform)/$($softpaqListCmd.platform)_cds.cab"
          $cacheDirAdvisory = $cacheDirOffline + "\$($softpaqListCmd.platform)"
          $filename = "$($softpaqListCmd.platform)_cds.cab"
          Write-Verbose "Trying to download Advisory Data Files..."
          try {
            $AdvisoryFile = Get-HPPrivateOfflineCacheFiles -url $url -FileName $filename -cacheDirOffline $cacheDirAdvisory -Expand
            Write-Verbose "Finish downloading Advisory Data Files - $AdvisoryFile"
          }
          catch {
            Write-Verbose "Trying to download AdvisoryData from FTP..."
            $baseurl = "https://ftp.hp.com/pub/caps-softpaq/cmit/imagepal/ref/"
            $url = $baseurl + "$($softpaqListCmd.platform)/$($softpaqListCmd.platform)_cds.cab"
            #$cacheDirAdvisory = $cacheDirOffline + "\$($softpaqListCmd.platform)"
            #$filename = "$($softpaqListCmd.platform)_cds.cab"
            $AdvisoryFile = Get-HPPrivateOfflineCacheFiles -url $url -FileName $filename -cacheDirOffline $cacheDirAdvisory -Expand
            Write-Verbose "Finish downloading Advisory Data Files - $AdvisoryFile"
            if (-not $AdvisoryFile) {
              $exception = $_.Exception
              switch ($repo[1].Settings.OnRemoteFileNotFound) {
                "LogAndContinue" {
                  [string]$data = formatSyncErrorMessageAsHtml $exception
                  Log ($data -split "`n")
                  send "Softpaq repository synchronization error" $data
                }
                # "Fail"
                default {
                  throw $exception
                }
              }
            }
          }

          # download Knowledge Base
          $url = "https://hpia.hpcloud.hp.com/kb/common/latest.cab"
          $cacheDirKb = $cacheDirOffline + "\kb\common"
          $filename = "latest.cab"
          Write-Verbose "Trying to download Knowledge Base..."
          try {
            $KnowledgeBase = Get-HPPrivateOfflineCacheFiles -url $url -FileName $filename -cacheDirOffline $cacheDirKb
            Write-Verbose "Finish downloading Knowledge Base - $KnowledgeBase"
          }
          catch {
            Write-Verbose "Trying to download Knowledge Base from FTP..."
            $url = "https://ftp.hp.com/pub/caps-softpaq/cmit/imagepal/kb/common/latest.cab"
            #$cacheDirKb = $cacheDirOffline + "\kb\common"
            #$filename = "latest.cab"
            $KnowledgeBase = Get-HPPrivateOfflineCacheFiles -url $url -FileName $filename -cacheDirOffline $cacheDirKb
            Write-Verbose "Finish downloading Knowledge Base - $KnowledgeBase"
            if (-not $KnowledgeBase) {
              $exception = $_.Exception
              switch ($repo[1].Settings.OnRemoteFileNotFound) {
                "LogAndContinue" {
                  [string]$data = formatSyncErrorMessageAsHtml $exception
                  Log ($data -split "`n")
                  send "Softpaq repository synchronization error" $data
                }
                # "Fail"
                default {
                  throw $exception
                }
              }
            }
          }
        }
      }
      else {
        Write-Host -ForegroundColor Cyan "Platform $($c.platform) doesn't exist. Please add a valid platform."
        Write-LogWarning "Platform $($c.platform) in not valid, and is was skipped."
      }
    }

    Write-Verbose ("Done with the list, repository is $($softpaqlist.Count) softpaqs.")
    [array]$softpaqlist = @($softpaqlist | Sort-Object -Unique -Property Id)
    Write-Verbose ("After trimming duplicates, we have $($softpaqlist.Count) softpaqs.")


    Write-Verbose ("Flushing the list of markers")
    FlushMarks
    Write-Verbose ("Writing new marks")

    # generate .mark file for each SoftPaq to be downloaded
    foreach ($sp in $softpaqList) {
      $number = $sp.id.ToLower().TrimStart("sp")
      TouchFile -File ".repository/mark/$number.mark"
    }

    Write-Verbose ("Starting download")
    $downloadCmd = @{}
    $downloadCmd.Quiet = $quiet
    $downloadCmd.verbose = $VerbosePreference

    Log "Download has started for $($softpaqlist.Count) softpaqs."
    foreach ($sp in $softpaqlist)
    {
      $downloadCmd.number = $sp.id.ToLower().TrimStart("sp")
      Write-Verbose "Working on data for softpaq $($downloadCmd.number)"
      try {
        log "Start downloading files for sp$($downloadCmd.number)."
        DownloadSoftpaq $downloadCmd -MaxRetries $repo[1].Settings.ExclusiveLockMaxRetries -Verbose:$VerbosePreference

        if ($OfflineCacheMode -eq "Enable") {
          log (" sp$($downloadCmd.number).html - Downloading Release Notes.")
          $ReleaseNotesurl = Get-HPPrivateItemUrl $downloadCmd.number "html"
          $target = "sp$($downloadCmd.number).html"
          $targetfile = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($target)
          Invoke-HPPrivateDownloadFile -url $ReleaseNotesurl -Target $targetfile
          log (" sp$($downloadCmd.number).html - Done Downloading Release Notes.")
        }
        log "Finish downloading files for sp$($downloadCmd.number)."
      }
      catch {
        $exception = $_.Exception

        switch ($repo[1].Settings.OnRemoteFileNotFound)
        {
          "LogAndContinue" {
            [string]$data = formatSyncErrorMessageAsHtml $exception
            log ($data -split "`n")
            send "Softpaq repository synchronization error" $data
          }
          # "Fail"
          default {
            throw $exception
          }
        }
      }
    }

    log "Repository sync has ended"
    Write-Verbose "Repository Sync has ended."
    
    Log "Repository Report creation started"
    Write-Verbose "Repository Report creation started."
    
    try {
      # get the configuration set for repository report if any
      $RepositoryReport = $repo[1].Settings.RepositoryReport
      if ($RepositoryReport) {
        $Format = $RepositoryReport
        New-RepositoryReport -Format $Format -RepositoryPath "$cwd" -OutputFile "$cwd\.repository\Contents.$Format"
        Log "Repository Report created as Contents.$Format"
        Write-Verbose "Repository Report created as Content.$Format."
      }
    }
    catch [System.IO.FileNotFoundException] {
      Write-Verbose "No data available to create Repository Report as directory '$(Get-Location)' does not contain any CVA files."
      Log "No data available to create Repository Report as directory '$(Get-Location)' does not contain any CVA files."
    }
    catch {
      Write-Verbose "Error in creating Repository Report"
      Log "Error in creating Repository Report."
    }
  }
  catch
  {
    err "Repository synchronization failed: $($_.Exception.Message)" $true
    [string]$data = FormatSyncErrorMessageAsHtml $_.Exception
    log ($data -split "`n")
    send "Softpaq repository synchronization error" $data
  }
}

function FormatSyncErrorMessageAsHtml ($exception)
{
  [string]$data = "An error occurred during softpaq synchronization.`n`n";
  $data += "The error was: <em>$($exception.Message)</em>`n"
  $data += "`nDetails:`n<pre>"
  $data += "<hr/>"
  $data += ($exception | Format-List -Force | Out-String)
  $data += "</pre>"
  $data += "<hr/>"
  $data
}

<#
.SYNOPSIS
    Cleanup repository
     
.DESCRIPTION
    Use Invoke-RepositoryCleanup to remove softpaqs from repository that are obsolete. These may be softpaqs that have been replaced
    by newer versions, or that no longer match the active repository filters.
 
.EXAMPLE
    Invoke-RepositoryCleanup
 
.LINK
    [Initialize-Repository](Initialize-Repository)
 
.LINK
    [Add-RepositoryFilter](Add-RepositoryFilter)
 
.LINK
    [Remove-RepositoryFilter](Remove-RepositoryFilter)
 
.LINK
    [Get-RepositoryInfo](Get-RepositoryInfo)
 
.LINK
    [Invoke-RepositorySync](Invoke-RepositorySync)
 
.LINK
    [Set-RepositoryNotificationConfiguration](Set-RepositoryNotificationConfiguration)
 
.LINK
    [Clear-RepositoryNotificationConfiguration](Clear-RepositoryNotificationConfiguration)
 
.LINK
    [Get-RepositoryNotificationConfiguration](Get-RepositoryNotificationConfiguration)
 
.LINK
    [Show-RepositoryNotificationConfiguration](Show-RepositoryNotificationConfiguration)
 
.LINK
    [Add-RepositorySyncFailureRecipient](Add-RepositorySyncFailureRecipient)
 
.LINK
    [Remove-RepositorySyncFailureRecipient](Remove-RepositorySyncFailureRecipient)
 
.LINK
    [Test-RepositoryNotificationConfiguration](Test-RepositoryNotificationConfiguration)
 
#>

function Invoke-RepositoryCleanup
{
  [CmdletBinding(HelpUri = "https://developers.hp.com/hp-client-management/doc/Invoke%E2%80%90RepositoryCleanup")]
  param()
  $repo = LoadRepository
  log ("Beginning repository cleanup")
  $deleted = 0

  try {
    Get-ChildItem "." -File | ForEach-Object {
      $name = $_.Name.ToLower().TrimStart("sp").Split('.')[0]
      if ($name -ne $null) {
        if (-not (Test-Path ".repository/mark/$name.mark" -PathType Leaf))
        {
          Write-Verbose "Deleting orphaned file $($_.Name)"
          Remove-Item $_.Name
          $deleted++
        }
        #else {
        # Write-Verbose "Softpaq $($_.Name) is still needed."
        #}
      }
    }
    log ("Completed repository cleanup, deleted $deleted files.")
  }
  catch {
    err ("Could not clean repository: $($_.Exception.Message)")
  }
}

<#
.SYNOPSIS
    Set the repository notification configuration.
 
.DESCRIPTION
    This function defines a notification SMTP server (and optionally, port) for an email server to be used to send
    failure notifications during unattended synchronization via Invoke-RepositorySync.
 
    One or more recipients can then be added via Add-RepositorySyncFailureRecipient.
 
    The directory must have been initialized via Initialize?Repository.
 
    The function must be invoked inside a directory initialized as a repository.
 
 
.PARAMETER Server
    The server name (or IP) for the outgoing mail (SMTP) server
 
.PARAMETER Port
    Specifies a port for the SMTP server. If not provided, the default IANA-assigned port 25 will be used.
 
.PARAMETER Tls
    specifies whether to use SSL/TLS. The value may be "true", "false", or "auto". Auto
    will automatically set SSL to true when the port is changed to a value different than 25. By default, TLS is false.
 
.PARAMETER UserName
    Specifies the SMTP server username for authenticated SMTP servers. If username is not specified, connection will be made without authentication.
 
.PARAMETER Password
    Specifies the SMTP server password for authenticated SMTP servers.
     
.PARAMETER From
    Specifies the email address from which the notification will appear to originate. Note that in servers may accept emails from specified
    domains only, or in some cases may require the email address to match the username.
 
.PARAMETER FromName
    Specifies the from address display name.
 
.PARAMETER RemoveCredentials
    Removes the SMTP server credentials without removing the entire mail server configuration.
 
.EXAMPLE
    Set-RepositoryNotificationConfiguration smtp.mycompany.com
 
.LINK
    [Initialize-Repository](Initialize-Repository)
 
.LINK
    [Add-RepositoryFilter](Add-RepositoryFilter)
 
.LINK
    [Remove-RepositoryFilter](Remove-RepositoryFilter)
 
.LINK
    [Get-RepositoryInfo](Get-RepositoryInfo)
 
.LINK
    [Invoke-RepositorySync](Invoke-RepositorySync)
 
.LINK
    [Invoke-RepositoryCleanup](Invoke-RepositoryCleanup)
 
.LINK
    [Clear-RepositoryNotificationConfiguration](Clear-RepositoryNotificationConfiguration)
 
.LINK
    [Get-RepositoryNotificationConfiguration](Get-RepositoryNotificationConfiguration)
 
.LINK
    [Show-RepositoryNotificationConfiguration](Show-RepositoryNotificationConfiguration)
 
.LINK
    [Add-RepositorySyncFailureRecipient](Add-RepositorySyncFailureRecipient)
 
.LINK
    [Remove-RepositorySyncFailureRecipient](Remove-RepositorySyncFailureRecipient)
 
.LINK
    [Test-RepositoryNotificationConfiguration](Test-RepositoryNotificationConfiguration)
 
#>

function Set-RepositoryNotificationConfiguration
{
  [CmdletBinding(HelpUri = "https://developers.hp.com/hp-client-management/doc/Set%E2%80%90RepositoryNotificationConfiguration")]
  param(
    [Parameter(Position = 0,Mandatory = $false)]
    [string]
    [ValidatePattern("^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$")]
    $Server = $null,

    [Parameter(Position = 1,Mandatory = $false)]
    [ValidateRange(1,65535)]
    [int]
    $Port = 0,

    [Parameter(Position = 2,Mandatory = $false)]
    [string]
    [ValidateSet('true','false','auto')]
    $Tls = $null,

    [Parameter(Position = 3,Mandatory = $false)]
    [string]
    $Username = $null,

    [Parameter(Position = 4,Mandatory = $false)]
    [string]
    $Password = $null,

    [Parameter(Position = 5,Mandatory = $false)]
    [string]
    [ValidatePattern("^\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$")]
    $From = $null,

    [Parameter(Position = 6,Mandatory = $false)]
    [string]
    $FromName = $null,

    [Parameter(Position = 7,Mandatory = $false)]
    [switch]
    $RemoveCredentials
  )

  Write-Verbose "Beginning notification configuration update"

  if ($RemoveCredentials.IsPresent -and ([string]::IsNullOrEmpty($UserName) -eq $false -or [string]::IsNullOrEmpty($Password) -eq $false))
  {
    err ("-removeCredentials may not be specified with -username or -password")
    return
  }

  $c = LoadRepository
  try {
    if (-not $c[0]) { return }

    Write-Verbose "Applying configuration"
    if ([string]::IsNullOrEmpty($Server) -eq $false) {
      Write-Verbose ("Setting SMTP Server to: $Server")
      $c[1].Notifications.server = $Server
    }

    if ($Port) {
      Write-Verbose ("Setting SMTP Server port to: $Port")
      $c[1].Notifications.port = $Port
    }

    if (-not [string]::IsNullOrEmpty($UserName)) {
      Write-Verbose ("Setting SMTP server credential(username) to: $UserName")
      $c[1].Notifications.username = $UserName
    }

    if (-not [string]::IsNullOrEmpty($Password) ) {
      Write-Verbose ("Setting SMTP server credential(password) to: (redacted)")
      $c[1].Notifications.password = ConvertTo-SecureString $Password -Force -asPlainText | ConvertFrom-SecureString
    }

    if ($RemoveCredentials.IsPresent)
    {
      Write-Verbose ("Clearing credentials from notification configuration")
      $c[1].Notifications.username = $null
      $c[1].Notifications.password = $null
    }

    switch ($Tls)
    {
      "auto" {
        if ($Port -ne 25) { $c[1].Notifications.tls = $true }
        else { $c[1].Notifications.tls = $false }
        Write-Verbose ("SMTP server SSL auto-calculated to: $($c[1].Notifications.tls)")
      }

      "true" {
        $c[1].Notifications.tls = $true
        Write-Verbose ("Setting SMTP SSL to: $($c[1].Notifications.tls)")
      }
      "false" {
        $c[1].Notifications.tls = $false
        Write-Verbose ("Setting SMTP SSL to: $($c[1].Notifications.tls)")
      }
    }
    if (-not [string]::IsNullOrEmpty($From)) {
      Write-Verbose ("Setting Mail from address to: $From")
      $c[1].Notifications.from = $From }
    if (-not [string]::IsNullOrEmpty($FromName)) {
      Write-Verbose ("Setting Mail from displayname to: $FromName")
      $c[1].Notifications.fromname = $FromName }

      WriteRepositoryFile  -obj $c[1]
    log ("Updated notification configuration")
  }
  catch {
    err ("Failed to modify repository configuration: $($_.Exception.Message)")
  }
}

<#
.SYNOPSIS
    Clear the repository notification configuration
 
.DESCRIPTION
    This function removes notification configuration from repository, in effect turning off notifications.
 
    The directory must have been initialized via Initialize-Repository and notification configuration must have been defined via Set-RepositoryNotificationConfiguration
 
    The function must be invoked inside a directory initialized as a repository.
 
.LINK
    [Initialize-Repository](Initialize-Repository)
 
.LINK
    [Add-RepositoryFilter](Add-RepositoryFilter)
 
.LINK
    [Remove-RepositoryFilter](Remove-RepositoryFilter)
 
.LINK
    [Get-RepositoryInfo](Get-RepositoryInfo)
 
.LINK
    [Invoke-RepositorySync](Invoke-RepositorySync)
 
.LINK
    [Invoke-RepositoryCleanup](Invoke-RepositoryCleanup)
 
.LINK
    [Set-RepositoryNotificationConfiguration](Set-RepositoryNotificationConfiguration)
 
.LINK
    [Get-RepositoryNotificationConfiguration](Get-RepositoryNotificationConfiguration)
 
.LINK
    [Show-RepositoryNotificationConfiguration](Show-RepositoryNotificationConfiguration)
 
.LINK
    [Add-RepositorySyncFailureRecipient](Add-RepositorySyncFailureRecipient)
 
.LINK
    [Remove-RepositorySyncFailureRecipient](Remove-RepositorySyncFailureRecipient)
 
.LINK
    [Test-RepositoryNotificationConfiguration](Test-RepositoryNotificationConfiguration)
 
.EXAMPLE
    Clear-RepositoryNotificationConfiguration
 
#>

function Clear-RepositoryNotificationConfiguration ()
{
  [CmdletBinding(HelpUri = "https://developers.hp.com/hp-client-management/doc/Clear%E2%80%90RepositoryNotificationConfiguration")]
  param()
  log "Clearing notification configuration"

  $c = LoadRepository
  try {
    if (-not $c[0]) { return }
    $c[1].Notifications = $null
    WriteRepositoryFile -obj $c[1]
    Write-Verbose ("Ok.")
  }
  catch {
    err ("Failed to modify repository configuration: $($_.Exception.Message)")
  }
}

<#
.SYNOPSIS
    Get an object representing the current notification configuration
 
.DESCRIPTION
    This function retrieves the current notification configuration as an object.
 
    The directory must have been initialized via Initialize-Repository and notification configuration must have been defined via Set-RepositoryNotificationConfiguration
 
  The function must be invoked inside a directory initialized as a repository.
   
.LINK
    [Initialize-Repository](Initialize-Repository)
 
.LINK
    [Add-RepositoryFilter](Add-RepositoryFilter)
 
.LINK
    [Remove-RepositoryFilter](Remove-RepositoryFilter)
 
.LINK
    [Get-RepositoryInfo](Get-RepositoryInfo)
 
.LINK
    [Invoke-RepositorySync](Invoke-RepositorySync)
 
.LINK
    [Invoke-RepositoryCleanup](Invoke-RepositoryCleanup)
 
.LINK
    [Set-RepositoryNotificationConfiguration](Set-RepositoryNotificationConfiguration)
 
.LINK
    [Clear-RepositoryNotificationConfiguration](Clear-RepositoryNotificationConfiguration)
 
.LINK
    [Show-RepositoryNotificationConfiguration](Show-RepositoryNotificationConfiguration)
 
.LINK
    [Add-RepositorySyncFailureRecipient](Add-RepositorySyncFailureRecipient)
 
.LINK
    [Test-RepositoryNotificationConfiguration](Test-RepositoryNotificationConfiguration)
 
.EXAMPLE
    $config = Get-RepositoryNotificationConfiguration
 
 
#>

function Get-RepositoryNotificationConfiguration ()
{
  [CmdletBinding(HelpUri = "https://developers.hp.com/hp-client-management/doc/Get%E2%80%90RepositoryNotificationConfiguration")]
  param()

  $c = LoadRepository
  if ((-not $c[0])  -or (-not $c[1].Notifications))
  {
    return $null
  }
  return $c[1].Notifications
}


<#
.SYNOPSIS
    Display the current notification configuration to screen
 
 
.DESCRIPTION
    This function retrieves the current notification configuration as user-friendly screen output.
 
    The directory must have been initialized via Initialize-Repository and notification configuration must have been defined via Set-RepositoryNotificationConfiguration
 
    The function must be invoked inside a directory initialized as a repository.
 
.LINK
    [Initialize-Repository](Initialize-Repository)
 
.LINK
    [Add-RepositoryFilter](Add-RepositoryFilter)
 
.LINK
    [Remove-RepositoryFilter](Remove-RepositoryFilter)
 
.LINK
    [Get-RepositoryInfo](Get-RepositoryInfo)
 
.LINK
    [Invoke-RepositorySync](Invoke-RepositorySync)
 
.LINK
    [Invoke-RepositoryCleanup](Invoke-RepositoryCleanup)
 
.LINK
    [Set-RepositoryNotificationConfiguration](Set-RepositoryNotificationConfiguration)
 
.LINK
    [Clear-RepositoryNotificationConfiguration](Clear-RepositoryNotificationConfiguration)
 
.LINK
    [Get-RepositoryNotificationConfiguration](Get-RepositoryNotificationConfiguration)
 
.LINK
    [Add-RepositorySyncFailureRecipient](Add-RepositorySyncFailureRecipient)
 
.LINK
    [Test-RepositoryNotificationConfiguration](Test-RepositoryNotificationConfiguration)
 
.EXAMPLE
    Show-RepositoryNotificationConfiguration
#>

function Show-RepositoryNotificationConfiguration ()
{
  [CmdletBinding(HelpUri = "https://developers.hp.com/hp-client-management/doc/Show%E2%80%90RepositoryNotificationConfiguration")]
  param()

  try {
    $c = Get-RepositoryNotificationConfiguration
    if (-not $c)
    {
      err ("Notifications are not configured.")
      return
    }

    if (-not [string]::IsNullOrEmpty($c.username)) {
      Write-Host "Notification server: smtp://$($c.username):<password-redacted>@$($c.server):$($c.port)"
    }
    else {
      Write-Host "Notification server: smtp://$($c.server):$($c.port)"
    }
    Write-Host "Email will arrive from $($c.from) with name `"$($c.fromname)`""

    if ((-not $c.addresses) -or (-not $c.addresses.Count))
    {
      Write-Host "There are no recipients configured"
      return
    }
    foreach ($r in $c.addresses)
    {
      Write-Host "Recipient: $r"
    }
  }
  catch {
    err ("Failed to read repository configuration: $($_.Exception.Message)")
  }

}

<#
.SYNOPSIS
    Add a recipient to be notified of failures
 
.DESCRIPTION
    This function adds an email address to the repository. On failures, notifications will be sent to this email address.
 
    The directory must have been initialized via Initialize?Repository and notification configured via Set-RepositoryNotificationConfiguration.
 
    The function must be invoked inside a directory initialized as a repository.
 
.PARAMETER To
    The email address to add
 
.LINK
    [Initialize-Repository](Initialize-Repository)
 
.LINK
    [Add-RepositoryFilter](Add-RepositoryFilter)
 
.LINK
    [Remove-RepositoryFilter](Remove-RepositoryFilter)
 
.LINK
    [Get-RepositoryInfo](Get-RepositoryInfo)
 
.LINK
    [Invoke-RepositorySync](Invoke-RepositorySync)
 
.LINK
    [Invoke-RepositoryCleanup](Invoke-RepositoryCleanup)
 
.LINK
    [Set-RepositoryNotificationConfiguration](Set-RepositoryNotificationConfiguration)
 
.LINK
    [Clear-RepositoryNotificationConfiguration](Clear-RepositoryNotificationConfiguration)
 
.LINK
    [Get-RepositoryNotificationConfiguration](Get-RepositoryNotificationConfiguration)
 
.LINK
    [Show-RepositoryNotificationConfiguration](Show-RepositoryNotificationConfiguration)
 
.LINK
    [Remove-RepositorySyncFailureRecipient](Remove-RepositorySyncFailureRecipient)
 
.LINK
    [Test-RepositoryNotificationConfiguration](Test-RepositoryNotificationConfiguration)
 
.EXAMPLE
    Add-RepositorySyncFailureRecipient -to someone@mycompany.com
 
#>

function Add-RepositorySyncFailureRecipient ()
{
  [CmdletBinding(HelpUri = "https://developers.hp.com/hp-client-management/doc/Add%E2%80%90RepositorySyncFailureRecipient")]
  param(
    [Parameter(Position = 0,Mandatory = $true)]
    [ValidatePattern("^\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$")]
    [string]
    $To
  )

  log "Adding '$To' as a recipient."
  $c = LoadRepository
  try {
    if (-not $c[0]) { return }

    if (-not $c[1].Notifications) {
      err ("Notifications are not configured")
      return
    }

    if (-not $c[1].Notifications.addresses) {
      $c[1].Notifications.addresses = $()
    }

    $c[1].Notifications.addresses += $To.Trim()
    $c[1].Notifications.addresses = $c[1].Notifications.addresses | Sort-Object -Unique
    WriteRepositoryFile -obj ($c[1] | Sort-Object -Unique)
  }
  catch {
    err ("Failed to modify repository configuration: $($_.Exception.Message)")
  }

}

<#
.SYNOPSIS
    Remove a recipient from notification list for the current repository.
 
 
.DESCRIPTION
    This function removes an email address as a recipient for synchronization failure messages.
 
    The directory must have been initialized via Initialize?Repository and notification configured via Set-RepositoryNotificationConfiguration.
 
    The function must be invoked inside a directory initialized as a repository.
 
.PARAMETER To
    The email address to remove
 
.LINK
    [Initialize-Repository](Initialize-Repository)
 
.LINK
    [Add-RepositoryFilter](Add-RepositoryFilter)
 
.LINK
    [Remove-RepositoryFilter](Remove-RepositoryFilter)
 
.LINK
    [Get-RepositoryInfo](Get-RepositoryInfo)
 
.LINK
    [Invoke-RepositorySync](Invoke-RepositorySync)
 
.LINK
    [Invoke-RepositoryCleanup](Invoke-RepositoryCleanup)
 
.LINK
    [Set-RepositoryNotificationConfiguration](Set-RepositoryNotificationConfiguration)
 
.LINK
    [Clear-RepositoryNotificationConfiguration](Clear-RepositoryNotificationConfiguration)
 
.LINK
    [Get-RepositoryNotificationConfiguration](Get-RepositoryNotificationConfiguration)
 
.LINK
    [Show-RepositoryNotificationConfiguration](Show-RepositoryNotificationConfiguration)
 
.LINK
    [Remove-RepositorySyncFailureRecipient](Remove-RepositorySyncFailureRecipient)
 
.LINK
    [Test-RepositoryNotificationConfiguration](Test-RepositoryNotificationConfiguration)
 
.EXAMPLE
    Remove-RepositorySyncFailureRecipient -to someone@mycompany.com
 
#>

function Remove-RepositorySyncFailureRecipient
{
  [CmdletBinding(HelpUri = "https://developers.hp.com/hp-client-management/doc/Remove%E2%80%90RepositorySyncFailureRecipient")]
  param(
    [Parameter(Position = 0,Mandatory = $true)]
    [ValidatePattern("^\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$")]
    [string]
    $To
  )
  log "Removing '$To' as a recipient."
  $c = LoadRepository
  try {
    if ($c[0] -eq $false) { return }

    if (-not $c[1].Notifications) {
      err ("Notifications are not configured")
      return
    }


    if (-not $c[1].Notifications.addresses) {
      $c[1].Notifications.addresses = $()
    }

    $c[1].Notifications.addresses = $c[1].Notifications.addresses | Where-Object { $_ -ne $To.Trim() } | Sort-Object -Unique
    WriteRepositoryFile -obj ($c[1] | Sort-Object -Unique)
  }
  catch {
    err ("Failed to modify repository configuration: $($_.Exception.Message)")
  }
}


<#
.SYNOPSIS
    Test the email notification configuration by sending a test email
 
.DESCRIPTION
    This function sends a test email using the current repository configuration and reports
    any errors associated with the send process. It is intended for debugging the email server configuration.
 
.LINK
    [Initialize-Repository](Initialize-Repository)
 
.LINK
    [Add-RepositoryFilter](Add-RepositoryFilter)
 
.LINK
    [Remove-RepositoryFilter](Remove-RepositoryFilter)
 
.LINK
    [Get-RepositoryInfo](Get-RepositoryInfo)
 
.LINK
    [Invoke-RepositorySync](Invoke-RepositorySync)
 
.LINK
    [Invoke-RepositoryCleanup](Invoke-RepositoryCleanup)
 
.LINK
    [Set-RepositoryNotificationConfiguration](Set-RepositoryNotificationConfiguration)
 
.LINK
    [Clear-RepositoryNotificationConfiguration](Clear-RepositoryNotificationConfiguration)
 
.LINK
    [Get-RepositoryNotificationConfiguration](Get-RepositoryNotificationConfiguration)
 
.LINK
    [Show-RepositoryNotificationConfiguration](Show-RepositoryNotificationConfiguration)
 
.LINK
    [Remove-RepositorySyncFailureRecipient](Remove-RepositorySyncFailureRecipient)
 
.EXAMPLE
    Test-RepositoryNotificationConfiguration
 
#>

function Test-RepositoryNotificationConfiguration
{
  [CmdletBinding(HelpUri = "https://developers.hp.com/hp-client-management/doc/Test%E2%80%90RepositoryNotificationConfiguration")]
  param()

  log ("test email started")
  send "Repository Failure Notification (Test only)" "No content." -html $false
  Write-Verbose ("Ok.")
}

<#
.SYNOPSIS
    Set repository configuration values
 
.DESCRIPTION
    This function sets various configuration options that control synchronization behavior.
 
.PARAMETER setting
    The setting to configure from 'OnRemoteFileNotFound', 'OfflineCacheMode' and 'RepositoryReport'.
 
.PARAMETER value
    The new value of the setting for OnRemoteFileNotFound. It can be from 'Fail' (default) and 'LogAndContinue'.
 
.PARAMETER CacheValue
  The new CacheValue of the setting for OfflineCacheMode. It can be from 'Disable' (default) and 'Enable'.
 
.PARAMETER Format
  The new value of the setting for RepositoryReport. It can be from 'CSV' (Default) ,'JSon', 'XML' and 'ExcelCSV'.
 
.LINK
  [Initialize-Repository](Initialize-Repository)
   
.LINK
  [Get-RepositoryConfiguration](Get-RepositoryConfiguration)
 
.Example
  Set-RepositoryConfiguration -Setting OnRemoteFileNotFound -Value LogAndContinue
 
.Example
  Set-RepositoryConfiguration -Setting OfflineCacheMode -CacheValue Enable
     
.Example
  Set-RepositoryConfiguration -Setting RepositoryReport -Format CSV
     
.NOTES
    Current event handlers supported:
 
  - _OnRemoteFileNotFound_ - indicates what should happen if an expected softpaq is not found
  on the remote site. The default is 'Fail' in which case the process will stop. Setting to 'LogAndContinue'
  will log the error, but the repository synchronization will continue.
 
  Current Repository Report functionality:
 
  Invoke-RepositorySync will create a report containing contents of the Repository in a format provided by RepositoryFormat Setting.
  Use RepositoryReport Setting with -Format to set the format in which the you would like the report to be generated. Default format is CSV.
  Report will be generated with a name Contents.<format> e.g. Contents.CSV inside the .repository directory
 
  To create a report outside the repository use New-RepositoryReport function.
#>

function Set-RepositoryConfiguration
{
  [CmdletBinding(HelpUri = "https://developers.hp.com/hp-client-management/doc/Set%E2%80%90RepositoryConfiguration")]
  param(
    [ValidateSet('OnRemoteFileNotFound','OfflineCacheMode','RepositoryReport')]
    [Parameter(ParameterSetName = "ErrorHandler",Position = 0,Mandatory = $true)]
    [Parameter(ParameterSetName = "CacheMode",Position = 0,Mandatory = $true)]
    [Parameter(ParameterSetName = "ReportHandler",Position = 0,Mandatory = $true)]
    [string]$Setting,

    [Parameter(ParameterSetName = "ErrorHandler",Position = 1,Mandatory = $true)]
    [ErrorHandling]$Value,

    [ValidateSet('Enable','Disable')]
    [Parameter(ParameterSetName = "CacheMode",Position = 1,Mandatory = $true)]
    [string]$CacheValue,

    [ValidateSet('CSV','JSon','XML','ExcelCSV')]
    [Parameter(ParameterSetName = "ReportHandler",Position = 1,Mandatory = $true)]
    [string]$Format
  )
  $c = LoadRepository
  if (-not $c[0]) { return }
  if ($Setting -eq "OnRemoteFileNotFound") {
    if (($Value -eq "Fail") -or ($Value -eq "LogAndContinue")) {
      $c[1].Settings. "${Setting}" = $Value
      WriteRepositoryFile  -obj $c[1]
      Write-Verbose ("Ok.")
    }
    else {
      Write-Host -ForegroundColor Magenta "Enter valid Value for $Setting."
      Write-LogWarning "Enter valid Value for $Setting."
    }
  }
  elseif ($Setting -eq "OfflineCacheMode") {
    if ($CacheValue) {
      $c[1].Settings. "${Setting}" = $CacheValue
      WriteRepositoryFile  -obj $c[1]
      Write-Verbose ("Ok.")
    }
    else {
      Write-Host -ForegroundColor Magenta "Enter valid CacheValue for $Setting."
      Write-LogWarning "Enter valid CacheValue for $Setting."
    }
  }
  elseif ($Setting -eq "RepositoryReport") {
    if($Format) {
      $c[1].Settings. "${Setting}" = $Format
      WriteRepositoryFile  -obj $c[1]
      Write-Verbose ("Ok.")
    }
    else {
      Write-Host -ForegroundColor Magenta "Enter valid Format for $Setting."
      Write-LogWarning "Enter valid Format for $Setting."
    }
  }
}

<#
.SYNOPSIS
    Get repository configuration values
 
.DESCRIPTION
    This function get various configuration options that control synchronization behavior.
 
.PARAMETER setting
  The setting to retrieve
 
.Example
  Get-RepositoryConfiguration -Setting OfflineCacheMode
 
.Example
  Get-RepositoryConfiguration -Setting OnRemoteFileNotFound
 
.Example
  Get-RepositoryConfiguration -Setting RepositoryReport
 
.LINK
    [Set-RepositoryConfiguration](Set-RepositoryConfiguration)
 
.LINK
  [Initialize-Repository](Initialize-Repository)
#>

function Get-RepositoryConfiguration
{
  [CmdletBinding(HelpUri = "https://developers.hp.com/hp-client-management/doc/Get%E2%80%90RepositoryConfiguration")]
  param(
    [Parameter(Position = 0,Mandatory = $true)]
    [string]
    [ValidateSet('OnRemoteFileNotFound','OfflineCacheMode','RepositoryReport')]
    $Setting
  )
  $c = LoadRepository
  if (-not $c[0]) { return }
  $c[1].Settings. "${Setting}"
}


<#
.SYNOPSIS
    Create a report from a repository directory
 
.DESCRIPTION
  This function scans a repository (or any directory containing CVAs and EXEs) and creates a report in one of the supported formats.
 
  Currently the supported formats are:
 
  - XML - Return an XML object
  - JSON - Return a JSON document
  - CSV - Return a CSV document
  - ExcelCSV - Return a CSV document containing an Excel hint that defines comma as a default separator. Use this only if you plan on opening the CSV file with excel.
 
  If a format is not specified, the function will return PowerShell objects to the pipeline.
 
 
.PARAMETER Format
    Specify the output format (CSV, JSON, or XML). If not specified, the function will return PowerShell objects.
 
.PARAMETER RepositoryPath
  By default, the function assumes the repository is in the current directory. This parameter allows specifying a different location
  for the repository.
 
.PARAMETER OutputFile
  Specify an output file for the function. Can only be specified together with "Format".
 
.EXAMPLE
  New-RepositoryReport -Format JSON -RepositoryPath c:\myrepository\softpaqs -OutputFile c:\repository\today.json
 
.NOTES
  The function currently supports scenarios where the Softpaq executable is stored under the format sp<softpaq-number>.exe.
 
#>

function New-RepositoryReport
{
  [CmdletBinding(HelpUri = "https://developers.hp.com/hp-client-management/doc/New%E2%80%90RepositoryReport")]
  param(
    [Parameter(Position = 0,Mandatory = $false)]
    [ValidateSet('CSV','JSon','XML','ExcelCSV')]
    [string]$Format,

    [Parameter(Position = 1,Mandatory = $false)]
    [System.IO.DirectoryInfo]$RepositoryPath = '.',

    [Parameter(Position = 2,Mandatory = $false)]
    [System.IO.FileInfo]$OutputFile
  )
  if ($OutputFile -and -not $format) { throw "OutputFile parameter requires a Format specifier" }
  $cvaList = Get-ChildItem -Path $RepositoryPath -Filter '*.cva'

  if (-not $cvaList -or -not $cvaList.Length)
  {
    throw [System.IO.FileNotFoundException]"Directory '$(Get-Location)' does not contain CVA files."
  }

  Write-Verbose "Processing $($cvaList.Length) CVAs"
  $results = $cvaList | ForEach-Object {
    $cva = Get-HPPrivateReadINI $_.FullName

    try {
      $exe = Get-ChildItem -Path ($cva.Softpaq.SoftpaqNumber.Trim() + ".exe") -ErrorAction stop
    }
    catch [System.Management.Automation.ItemNotFoundException]{
      $exe = $null
    }

    [PSCustomObject]@{
      Softpaq = $cva.Softpaq.SoftpaqNumber
      Vendor = $cva.General.VendorName
      Title = $cva. "Software Title".US
      Type = if ($Cva.General.category.Contains("-")) { $Cva.General.category.Substring(0,$Cva.General.category.IndexOf('-')).Trim() } else { $Cva.General.category }
      Version = "$($cva.General.Version) Rev.$($cva.General.Revision)"
      Downloaded = if ($exe) { $exe.CreationTime } else { "" }
      Size = if ($exe) { "$($exe.Length)" } else { "" }
    }
  }
  switch ($format)
  {
    "CSV" {
      $r = $results | ConvertTo-Csv -NoTypeInformation
    }
    "ExcelCSV" {

      $r = $results | ConvertTo-Csv -NoTypeInformation
      $r = [string[]]"sep=," + $r
    }
    "JSon" {
      $r = $results | ConvertTo-Json
    }
    "XML" {
      $r = $results | ConvertTo-Xml -NoTypeInformation
    }
    default {
      return $results
    }
  }

  if ($OutputFile) {
    if ($format -eq "xml") { $r = $r.OuterXML }
    $r | Out-File -FilePath $OutputFile -Encoding utf8
  }
  else { $r }
}






# SIG # Begin signature block
# MIIakgYJKoZIhvcNAQcCoIIagzCCGn8CAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCAbbFeYYPsEASR1
# QX0PsyDuuHrMPoY0bUWFbEQcRhY64aCCCm8wggUwMIIEGKADAgECAhAECRgbX9W7
# ZnVTQ7VvlVAIMA0GCSqGSIb3DQEBCwUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQK
# EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNV
# BAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0xMzEwMjIxMjAwMDBa
# Fw0yODEwMjIxMjAwMDBaMHIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy
# dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xMTAvBgNVBAMTKERpZ2lD
# ZXJ0IFNIQTIgQXNzdXJlZCBJRCBDb2RlIFNpZ25pbmcgQ0EwggEiMA0GCSqGSIb3
# DQEBAQUAA4IBDwAwggEKAoIBAQD407Mcfw4Rr2d3B9MLMUkZz9D7RZmxOttE9X/l
# qJ3bMtdx6nadBS63j/qSQ8Cl+YnUNxnXtqrwnIal2CWsDnkoOn7p0WfTxvspJ8fT
# eyOU5JEjlpB3gvmhhCNmElQzUHSxKCa7JGnCwlLyFGeKiUXULaGj6YgsIJWuHEqH
# CN8M9eJNYBi+qsSyrnAxZjNxPqxwoqvOf+l8y5Kh5TsxHM/q8grkV7tKtel05iv+
# bMt+dDk2DZDv5LVOpKnqagqrhPOsZ061xPeM0SAlI+sIZD5SlsHyDxL0xY4PwaLo
# LFH3c7y9hbFig3NBggfkOItqcyDQD2RzPJ6fpjOp/RnfJZPRAgMBAAGjggHNMIIB
# yTASBgNVHRMBAf8ECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBhjATBgNVHSUEDDAK
# BggrBgEFBQcDAzB5BggrBgEFBQcBAQRtMGswJAYIKwYBBQUHMAGGGGh0dHA6Ly9v
# Y3NwLmRpZ2ljZXJ0LmNvbTBDBggrBgEFBQcwAoY3aHR0cDovL2NhY2VydHMuZGln
# aWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNydDCBgQYDVR0fBHow
# eDA6oDigNoY0aHR0cDovL2NybDQuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJl
# ZElEUm9vdENBLmNybDA6oDigNoY0aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0Rp
# Z2lDZXJ0QXNzdXJlZElEUm9vdENBLmNybDBPBgNVHSAESDBGMDgGCmCGSAGG/WwA
# AgQwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAK
# BghghkgBhv1sAzAdBgNVHQ4EFgQUWsS5eyoKo6XqcQPAYPkt9mV1DlgwHwYDVR0j
# BBgwFoAUReuir/SSy4IxLVGLp6chnfNtyA8wDQYJKoZIhvcNAQELBQADggEBAD7s
# DVoks/Mi0RXILHwlKXaoHV0cLToaxO8wYdd+C2D9wz0PxK+L/e8q3yBVN7Dh9tGS
# dQ9RtG6ljlriXiSBThCk7j9xjmMOE0ut119EefM2FAaK95xGTlz/kLEbBw6RFfu6
# r7VRwo0kriTGxycqoSkoGjpxKAI8LpGjwCUR4pwUR6F6aGivm6dcIFzZcbEMj7uo
# +MUSaJ/PQMtARKUT8OZkDCUIQjKyNookAv4vcn4c10lFluhZHen6dGRrsutmQ9qz
# sIzV6Q3d9gEgzpkxYz0IGhizgZtPxpMQBvwHgfqL2vmCSfdibqFT+hKUGIUukpHq
# aGxEMrJmoecYpJpkUe8wggU3MIIEH6ADAgECAhAFUi3UAAgCGeslOwtVg52XMA0G
# CSqGSIb3DQEBCwUAMHIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJ
# bmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xMTAvBgNVBAMTKERpZ2lDZXJ0
# IFNIQTIgQXNzdXJlZCBJRCBDb2RlIFNpZ25pbmcgQ0EwHhcNMjEwMzIyMDAwMDAw
# WhcNMjIwMzMwMjM1OTU5WjB1MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZv
# cm5pYTESMBAGA1UEBxMJUGFsbyBBbHRvMRAwDgYDVQQKEwdIUCBJbmMuMRkwFwYD
# VQQLExBIUCBDeWJlcnNlY3VyaXR5MRAwDgYDVQQDEwdIUCBJbmMuMIIBIjANBgkq
# hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtJ+rYUkseHcrB2M/GyomCEyKn9tCyfb+
# pByq/Jyf5kd3BGh+/ULRY7eWmR2cjXHa3qBAEHQQ1R7sX85kZ5sl2ukINGZv5jEM
# 04ERNfPoO9+pDndLWnaGYxxZP9Y+Icla09VqE/jfunhpLYMgb2CuTJkY2tT2isWM
# EMrKtKPKR5v6sfhsW6WOTtZZK+7dQ9aVrDqaIu+wQm/v4hjBYtqgrXT4cNZSPfcj
# 8W/d7lFgF/UvUnZaLU5Z/+lYbPf+449tx+raR6GD1WJBAzHcOpV6tDOI5tQcwHTo
# jJklvqBkPbL+XuS04IUK/Zqgh32YZvDnDohg0AEGilrKNiMes5wuAQIDAQABo4IB
# xDCCAcAwHwYDVR0jBBgwFoAUWsS5eyoKo6XqcQPAYPkt9mV1DlgwHQYDVR0OBBYE
# FD4tECf7wE2l8kA6HTvOgkbo33MvMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAK
# BggrBgEFBQcDAzB3BgNVHR8EcDBuMDWgM6Axhi9odHRwOi8vY3JsMy5kaWdpY2Vy
# dC5jb20vc2hhMi1hc3N1cmVkLWNzLWcxLmNybDA1oDOgMYYvaHR0cDovL2NybDQu
# ZGlnaWNlcnQuY29tL3NoYTItYXNzdXJlZC1jcy1nMS5jcmwwSwYDVR0gBEQwQjA2
# BglghkgBhv1sAwEwKTAnBggrBgEFBQcCARYbaHR0cDovL3d3dy5kaWdpY2VydC5j
# b20vQ1BTMAgGBmeBDAEEATCBhAYIKwYBBQUHAQEEeDB2MCQGCCsGAQUFBzABhhho
# dHRwOi8vb2NzcC5kaWdpY2VydC5jb20wTgYIKwYBBQUHMAKGQmh0dHA6Ly9jYWNl
# cnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFNIQTJBc3N1cmVkSURDb2RlU2lnbmlu
# Z0NBLmNydDAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4IBAQBZca1CZfgn
# DucOwEDZk0RXqb8ECXukFiih/rPQ+T5Xvl3bZppGgPnyMyQXXC0fb94p1socJzJZ
# fn7rEQ4tHxL1vpBvCepB3Jq+i3A8nnJFHSjY7aujglIphfGND97U8OUJKt2jwnni
# EgsWZnFHRI9alEvfGEFyFrAuSo+uBz5oyZeOAF0lRqaRht6MtGTma4AEgq6Mk/iP
# LYIIZ5hXmsGYWtIPyM8Yjf//kLNPRn2WeUFROlboU6EH4ZC0rLTMbSK5DV+xL/e8
# cRfWL76gd/qj7OzyJR7EsRPg92RQUC4RJhCrQqFFnmI/K84lPyHRgoctAMb8ie/4
# X6KaoyX0Z93PMYIPeTCCD3UCAQEwgYYwcjELMAkGA1UEBhMCVVMxFTATBgNVBAoT
# DERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTExMC8GA1UE
# AxMoRGlnaUNlcnQgU0hBMiBBc3N1cmVkIElEIENvZGUgU2lnbmluZyBDQQIQBVIt
# 1AAIAhnrJTsLVYOdlzANBglghkgBZQMEAgEFAKB8MBAGCisGAQQBgjcCAQwxAjAA
# MBkGCSqGSIb3DQEJAzEMBgorBgEEAYI3AgEEMBwGCisGAQQBgjcCAQsxDjAMBgor
# BgEEAYI3AgEVMC8GCSqGSIb3DQEJBDEiBCD5qbGmFV/KYUGVmY5Xxzv3zeJ0Vz5W
# KoOpDqDH7E+2mTANBgkqhkiG9w0BAQEFAASCAQBrQ05aoO3gM2WvbI3Q4hJrQSg1
# aJVBL722pdyZr67vBG85ZVL9DWhyhrsW7WOdXLyB24jnV5VIyroNmQ5kkBHiiHVs
# WLLf+Is2Qsyul010+CrNckavAgjrsjPsJMYdu/GAnoS+gPyMZHe5z6n0huG+cFk7
# ORnEYUIrqwx5X+qfaRx3kcVHDB5wet/qSkHsA9HHyG6r3GazJIUHjm5Ucfz6hgPv
# cPBjMzOy87bG1ZYcfq+HggN4NOunIqufkiCLbuuYWMQTvA2Z9LFqmsbI6e7+rqp8
# sjmAAK+R4ANUttzleLzcAakXk8apAoiqEkicdGGK6xVi5RPiQAW17g8492xcoYIN
# RTCCDUEGCisGAQQBgjcDAwExgg0xMIINLQYJKoZIhvcNAQcCoIINHjCCDRoCAQMx
# DzANBglghkgBZQMEAgEFADB4BgsqhkiG9w0BCRABBKBpBGcwZQIBAQYJYIZIAYb9
# bAcBMDEwDQYJYIZIAWUDBAIBBQAEIGIyaJUBLBEscgaYdQT36thSOVC0oI1uDXQp
# uyBs70IAAhEA++sBOrmpVXW2IJjlsQOP+RgPMjAyMTA0MTkxNzA0MzVaoIIKNzCC
# BP4wggPmoAMCAQICEA1CSuC+Ooj/YEAhzhQA8N0wDQYJKoZIhvcNAQELBQAwcjEL
# MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3
# LmRpZ2ljZXJ0LmNvbTExMC8GA1UEAxMoRGlnaUNlcnQgU0hBMiBBc3N1cmVkIElE
# IFRpbWVzdGFtcGluZyBDQTAeFw0yMTAxMDEwMDAwMDBaFw0zMTAxMDYwMDAwMDBa
# MEgxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjEgMB4GA1UE
# AxMXRGlnaUNlcnQgVGltZXN0YW1wIDIwMjEwggEiMA0GCSqGSIb3DQEBAQUAA4IB
# DwAwggEKAoIBAQDC5mGEZ8WK9Q0IpEXKY2tR1zoRQr0KdXVNlLQMULUmEP4dyG+R
# awyW5xpcSO9E5b+bYc0VkWJauP9nC5xj/TZqgfop+N0rcIXeAhjzeG28ffnHbQk9
# vmp2h+mKvfiEXR52yeTGdnY6U9HR01o2j8aj4S8bOrdh1nPsTm0zinxdRS1LsVDm
# QTo3VobckyON91Al6GTm3dOPL1e1hyDrDo4s1SPa9E14RuMDgzEpSlwMMYpKjIjF
# 9zBa+RSvFV9sQ0kJ/SYjU/aNY+gaq1uxHTDCm2mCtNv8VlS8H6GHq756WwogL0sJ
# yZWnjbL61mOLTqVyHO6fegFz+BnW/g1JhL0BAgMBAAGjggG4MIIBtDAOBgNVHQ8B
# Af8EBAMCB4AwDAYDVR0TAQH/BAIwADAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDBB
# BgNVHSAEOjA4MDYGCWCGSAGG/WwHATApMCcGCCsGAQUFBwIBFhtodHRwOi8vd3d3
# LmRpZ2ljZXJ0LmNvbS9DUFMwHwYDVR0jBBgwFoAU9LbhIB3+Ka7S5GGlsqIlssgX
# NW4wHQYDVR0OBBYEFDZEho6kurBmvrwoLR1ENt3janq8MHEGA1UdHwRqMGgwMqAw
# oC6GLGh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9zaGEyLWFzc3VyZWQtdHMuY3Js
# MDKgMKAuhixodHRwOi8vY3JsNC5kaWdpY2VydC5jb20vc2hhMi1hc3N1cmVkLXRz
# LmNybDCBhQYIKwYBBQUHAQEEeTB3MCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5k
# aWdpY2VydC5jb20wTwYIKwYBBQUHMAKGQ2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0
# LmNvbS9EaWdpQ2VydFNIQTJBc3N1cmVkSURUaW1lc3RhbXBpbmdDQS5jcnQwDQYJ
# KoZIhvcNAQELBQADggEBAEgc3LXpmiO85xrnIA6OZ0b9QnJRdAojR6OrktIlxHBZ
# vhSg5SeBpU0UFRkHefDRBMOG2Tu9/kQCZk3taaQP9rhwz2Lo9VFKeHk2eie38+dS
# n5On7UOee+e03UEiifuHokYDTvz0/rdkd2NfI1Jpg4L6GlPtkMyNoRdzDfTzZTlw
# S/Oc1np72gy8PTLQG8v1Yfx1CAB2vIEO+MDhXM/EEXLnG2RJ2CKadRVC9S0yOIHa
# 9GCiurRS+1zgYSQlT7LfySmoc0NR2r1j1h9bm/cuG08THfdKDXF+l7f0P4TrweOj
# SaH6zqe/Vs+6WXZhiV9+p7SOZ3j5NpjhyyjaW4emii8wggUxMIIEGaADAgECAhAK
# oSXW1jIbfkHkBdo2l8IVMA0GCSqGSIb3DQEBCwUAMGUxCzAJBgNVBAYTAlVTMRUw
# EwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20x
# JDAiBgNVBAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0xNjAxMDcx
# MjAwMDBaFw0zMTAxMDcxMjAwMDBaMHIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxE
# aWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xMTAvBgNVBAMT
# KERpZ2lDZXJ0IFNIQTIgQXNzdXJlZCBJRCBUaW1lc3RhbXBpbmcgQ0EwggEiMA0G
# CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC90DLuS82Pf92puoKZxTlUKFe2I0rE
# DgdFM1EQfdD5fU1ofue2oPSNs4jkl79jIZCYvxO8V9PD4X4I1moUADj3Lh477sym
# 9jJZ/l9lP+Cb6+NGRwYaVX4LJ37AovWg4N4iPw7/fpX786O6Ij4YrBHk8JkDbTuF
# fAnT7l3ImgtU46gJcWvgzyIQD3XPcXJOCq3fQDpct1HhoXkUxk0kIzBdvOw8YGqs
# LwfM/fDqR9mIUF79Zm5WYScpiYRR5oLnRlD9lCosp+R1PrqYD4R/nzEU1q3V8mTL
# ex4F0IQZchfxFwbvPc3WTe8GQv2iUypPhR3EHTyvz9qsEPXdrKzpVv+TAgMBAAGj
# ggHOMIIByjAdBgNVHQ4EFgQU9LbhIB3+Ka7S5GGlsqIlssgXNW4wHwYDVR0jBBgw
# FoAUReuir/SSy4IxLVGLp6chnfNtyA8wEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNV
# HQ8BAf8EBAMCAYYwEwYDVR0lBAwwCgYIKwYBBQUHAwgweQYIKwYBBQUHAQEEbTBr
# MCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wQwYIKwYBBQUH
# MAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJ
# RFJvb3RDQS5jcnQwgYEGA1UdHwR6MHgwOqA4oDaGNGh0dHA6Ly9jcmw0LmRpZ2lj
# ZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcmwwOqA4oDaGNGh0dHA6
# Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcmww
# UAYDVR0gBEkwRzA4BgpghkgBhv1sAAIEMCowKAYIKwYBBQUHAgEWHGh0dHBzOi8v
# d3d3LmRpZ2ljZXJ0LmNvbS9DUFMwCwYJYIZIAYb9bAcBMA0GCSqGSIb3DQEBCwUA
# A4IBAQBxlRLpUYdWac3v3dp8qmN6s3jPBjdAhO9LhL/KzwMC/cWnww4gQiyvd/Mr
# HwwhWiq3BTQdaq6Z+CeiZr8JqmDfdqQ6kw/4stHYfBli6F6CJR7Euhx7LCHi1lss
# FDVDBGiy23UC4HLHmNY8ZOUfSBAYX4k4YU1iRiSHY4yRUiyvKYnleB/WCxSlgNcS
# R3CzddWThZN+tpJn+1Nhiaj1a5bA9FhpDXzIAbG5KHW3mWOFIoxhynmUfln8jA/j
# b7UBJrZspe6HUSHkWGCbugwtK22ixH67xCUrRwIIfEmuE7bhfEJCKMYYVs9BNLZm
# XbZ0e/VWMyIvIjayS6JKldj1po5SMYICTTCCAkkCAQEwgYYwcjELMAkGA1UEBhMC
# VVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0
# LmNvbTExMC8GA1UEAxMoRGlnaUNlcnQgU0hBMiBBc3N1cmVkIElEIFRpbWVzdGFt
# cGluZyBDQQIQDUJK4L46iP9gQCHOFADw3TANBglghkgBZQMEAgEFAKCBmDAaBgkq
# hkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwHAYJKoZIhvcNAQkFMQ8XDTIxMDQxOTE3
# MDQzNVowKwYLKoZIhvcNAQkQAgwxHDAaMBgwFgQU4deCqOGRvu9ryhaRtaq0lKYk
# m/MwLwYJKoZIhvcNAQkEMSIEIPYgf5FVOpzr3kFphmSjrHyQjFM6Jx7xkxHWbINj
# snvBMA0GCSqGSIb3DQEBAQUABIIBAAJomdKw1pvU7yEoaXUUuG5bOWCsIxXWsEn/
# pF2/f6htgYXRjpKPg+HQs4EW1TAZsf/Wq5CNCDiRCzy3FOvpOkJQIFJbwYEhdJgG
# KARotk0Y9BBkMQJqeLrrQHatjcd7bun4054ZyIRQuzMjmcdRtr3PgyEmrOF5vcyJ
# I9Jhyc3g8fjDepiTGi+k1W14cSyjU24280SYiL4F8+UW/VpRQQdXjEsKGZZmlT0I
# l4pb2mr/GVTDaGZYYfjnypbUf37jZbL4gG3uhuyeubVkTXunkw4Y4nn9pX+Q8AON
# kTyZHQMB4vcAcwz7QlQbdWFP5hDGjmWlGEq2Cy6bgBjRo/gdaxQ=
# SIG # End signature block