Archive/DeployLibNorEvo-1.1.7.psm1

<#
.SYNOPSIS
  This script contains all functions, including deploy session initialization
 
.DESCRIPTION
  Script is "dot-sourced" from installation script in order to load functions.
 
  Start-NEDeploySession - Logging and temporary folder management. Inventory.
  Write-NELog - Writes events to script log
  Set-NELogonMaintenanceMessage - Set interactive logon message text
  Invoke-NEFileTransfer - Manages file transfers
  Invoke-NEWIMoperations - Handles mounting of WIM files to $Global:WorkingFolder\Mount folder
  Install-NEWingetApplication - Install Winget application by ID
  Uninstall-NEWingetApplication - Uninstall Winget application by ID
  Invoke-NEWingetInstallOrUpdate - Install or update Winget to min version. Called by functions Install-NEWingetApplication and Uninstall-NEWingetApplication
  Invoke-NERemovePublicDesktopShortcuts - Removes shortcuts on public desktop added during install session
  Edit-NEIniFileContent - Manipulate content of INI files
  Add-NEExitCode - Adds exit codes to be summarized to the script's exit code
  Invoke-NEInstallResult - Evaluates exit code and converts it to 0 (succeeded) or 1 (failure)
  Invoke-NEResetDeployEnvironment - Dismounts WIM files, closes processes and removes AppDeploy* variables
  Invoke-NEFinalAction - Called at last line of script to summarize exit codes, dismount wim files and remove temp directory
   
 
.NOTES
  Version history
  1.1.7 2024-01-31 - Everything works excempt install by Winget
  1.1.3 2024-01-31 - Merges from template 1.0 and fixes
  1.1.2 2024-01-24 - Updated and new functions merged from template 1.0
  1.1.0 2023-10-09 - Battling the Publish-Module function
  1.0.0 2023-10-09 - Initial release (MKa)
 
#>


Function Start-NEDeploySession {
  [CmdletBinding()]
  
  $VerbosePreference = "Continue"
  #$VerbosePreference = "SilentlyContinue"

  ### Logging
  # Set logs folder
  $Global:Logpath = "C:\Logs"
  # Create log directory if missing (used for script and application logs)
  if (!( Test-Path $Global:Logpath -PathType Container )) { New-Item -ItemType "directory" -Path $Global:Logpath  | Out-Null }
  # Script log name
  $Script:ScriptLogFile = $Global:Logpath + "\" + "$Global:ScriptName.log"
  # Rotate Script Log file if exist
  If (Test-Path -Path $Script:ScriptLogFile) { Get-ChildItem -Path $Script:ScriptLogFile | Where-Object { $_.BaseName -notmatch '\d{8}_\d{4}$' } | Rename-Item -NewName { "$($_.BaseName)-$($_.LastWriteTime.ToString('yyyyMMdd_HHmmss'))$($_.Extension)" }}
  # Create Script Log File
  if (!(Test-Path $Script:ScriptLogFile)) { New-Item $Script:ScriptLogFile -Type File | Out-Null }
  
  # Create a session unique folder name and create folder
  $Global:WorkingFolder = "C:\Deploy-" + (-join ((48..57) + (97..122) | Get-Random -Count 8 | ForEach-Object {[char]$_}))
  New-Item -ItemType "directory" -Path $Global:WorkingFolder | Out-Null

  # Check if MDT/SCCM session
  try {
    $TSenv = New-Object -COMObject Microsoft.SMS.TSEnvironment
    $Script:MDTIntegration = $true
  }
  catch { $Script:MDTIntegration = $false }
  finally {
    if ($Script:MDTIntegration) { $Script:TSLogpath = $TSenv.Value("LogPath") }
  }

  # Get originating process
  $repeat = $true
  $Process = Get-WmiObject Win32_Process -Filter "ProcessId = $pid"
  $ProcessTree = "$($Process.Name) (ID: $($Process.ProcessId))"
  # Get parent sessions recursively
  while ($repeat -eq $true) {
    $ProcessId = $Process.ParentProcessId
    $Process = Get-WmiObject Win32_Process -Filter "ProcessId = $ProcessId"
    if ($null -eq $Process -or $ProcessTree -like "*RuntimeBroker*") { $repeat = $false }
    else { $ProcessTree += "`n$($Process.Name) (ID: $($Process.ProcessId))" }
  }
  # Translate $Process into known processes
  switch -Wildcard ($ProcessTree) {
    "*KaseyaTaskRunnerx64*" { $OriginatingProcess = "Kaseya" }
    "*upKeeper*" { $OriginatingProcess = "upKeeper" }
    "*Intune*" { $OriginatingProcess = "Intune" }
    Default { $OriginatingProcess = "Unknown" }
  }
    
  # Check pending reboot status
  $PendingReboot = $false
  if ((get-childitem "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update" | Select-Object Name) -like '*RebootRequired*') { $PendingReboot = "$true" }
  if ((get-childitem "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing" | Select-Object Name) -like '*RebootPending*') { $PendingReboot = "$true" }

  # Inventory public desktop shortcuts pre installation
  $script:ShortcutsPre = (Get-ChildItem -Path "C:\Users\Public\Desktop" -Filter "*.lnk").Fullname

  # Check if user executing script is elevated ($UserElevated)
  $e = New-Object System.Security.Principal.WindowsPrincipal([System.Security.Principal.WindowsIdentity]::GetCurrent())
  if ($e.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator)) { $UserElevated = $true }
  else { $UserElevated = $false }

  # Remove ExitCode variable (if exists)
  if ($script:DeployExitCode) { Remove-Variable DeployExitCode -Scope Script }
  
  # Set initial error code value = 0
  $script:DeployExitCode = 0

  # Perform inventory
  $InvComputerMake = (Get-WmiObject -Class:Win32_ComputerSystem).Manufacturer
  $InvComputerSerialNumber = (Get-WmiObject win32_bios).SerialNumber
  $InvComputerModel = (Get-WmiObject -Class:Win32_ComputerSystem).Model
  $InvComputerSystemfamily = (Get-WmiObject -Class:Win32_ComputerSystem).SystemFamily
  $InvSysdrvFreeSpace = [math]::Round(((Get-WmiObject -Class Win32_logicaldisk -Filter "DeviceID = 'C:'").Freespace /1GB),0)
  $InvFunctionsLibVer = (Get-Module | Where-Object { $_.Name -eq "DeployLibNorEvo" }).Version.ToString()
  if (!($User)) { $User = "N/A" }
  
  # Log system and session info
  Write-NELog " "
  Write-NELog "### System"
  Write-NELog "Computer Name: $env:computername"
  Write-NELog "Serial Number: $InvComputerSerialNumber"
  Write-NELog "Make: $InvComputerMake"
  Write-NELog "Model: $InvComputerModel"
  Write-NELog "Model info: $InvComputerSystemfamily"
  Write-NELog " "
  Write-NELog "### Session"
  Write-NELog "Originating Process: $OriginatingProcess"
  Write-NELog "Functions Library Version: $InvFunctionsLibVer"
  Write-NELog "MDT/SCCM Session: $Script:MDTIntegration"
  Write-NELog "ScriptDir: $Global:ScriptDir"
  Write-NELog "ScriptName: $Global:ScriptName"
  Write-NELog "Script Log: $Script:ScriptLogFile"
  Write-NELog "WorkingFolder: $Global:WorkingFolder"
  Write-NELog "Available disk space: $InvSysdrvFreeSpace GB"
  Write-NELog "Pending reboot: $PendingReboot"
  Write-NELog "Credentails supplied for user: $User"
  Write-NELog "Script running elevated: $UserElevated"
  Write-NELog " "
  Write-NELog "### Custom actions"
}


Function Write-NELog {
  [CmdletBinding()]
  param (
    [Parameter(Mandatory = $true)] [string]$Message,         
    [Parameter()] [ValidateSet(1, 2, 3)] [string]$LogLevel = 1,
    [Parameter()] [string]$WriteToScreen = $true
  )
  $TimeGenerated = "$(Get-Date -Format HH:mm:ss).$((Get-Date).Millisecond)+000"
  $TimeGeneratedHMS = "$(Get-Date -Format HH:mm:ss)"
  $Line = '<![LOG[{0}]LOG]!><time="{1}" date="{2}" component="{3}" context="" type="{4}" thread="" file="">'
  $LineFormat = $Message, $TimeGenerated, (Get-Date -Format MM-dd-yyyy), "$($Global:ScriptName | Split-Path -Leaf):$($MyInvocation.ScriptLineNumber)", $LogLevel
  $Line = $Line -f $LineFormat
  Add-Content -Value $Line -Path $Script:ScriptLogFile

  if($WriteToScreen -eq $true) {
    switch ($LogLevel) {
      '1' { Write-Host $TimeGeneratedHMS "-" $Message -ForegroundColor Gray }
      '2' { Write-Host $TimeGeneratedHMS "-" $Message -ForegroundColor Yellow }
      '3' { Write-Host $TimeGeneratedHMS "-" $Message -ForegroundColor Red }
      Default {}
    }           
  }
# if ($writetolistbox -eq $true) { $result1.Items.Add("$Message") }
}


Function Set-NELogonMaintenanceMessage {
  [CmdletBinding()]
  Param(
    [Parameter(Mandatory=$true)] [string]$AppName
  )
  Write-NELog "Setting logon message for application $AppName"
# [void](New-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" -Name "LegalNoticeCaption" -PropertyType String -Value "Systemunderh�ll" -Force)
# [void](New-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" -Name "LegalNoticeText" -PropertyType String -Value "Applikationen $AppName $Action. V�nligen v�nta med att logga in tills datorn har startas om och detta meddelande inte l�gre visas n�r du trycker Ctrl + Alt + Delete f�r att l�sa upp datorn." -Force)

  [void](New-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" -Name "LegalNoticeCaption" -PropertyType String -Value "System Maintenence" -Force)
  [void](New-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" -Name "LegalNoticeText" -PropertyType String -Value "Applikationen $AppName $Action. V�nligen v�nta med att logga in tills datorn har startas om och detta meddelande inte l�gre visas n�r du trycker Ctrl + Alt + Delete f�r att l�sa upp datorn." -Force)
}


Function Invoke-NEFileTransfer {
  [CmdletBinding()]
  Param(
      [Parameter(Mandatory=$true)] [string]$URL,
      [Parameter(Mandatory=$false)] [string]$DestinationFileName,
      [Parameter(Mandatory=$false)] [INT]$DownloadAttempts = 3,
      [Parameter(Mandatory=$false)] [INT]$VerifySeconds = 5
  )
  # Create Secure String for package downloads if -User and -Pass arguments were passed from command line
  if (($User) -and ($Pass)) {
    $SecPass = ConvertTo-SecureString $Pass -asplaintext -force
    $Credentials = new-object -typename System.Management.Automation.PSCredential -argumentlist $User,$SecPass
  }
  #Inital values
  $a = 1 
  #$result.InternalErrorCode = "1"
  while ($result.InternalErrorCode -ne 0 -and $a -le $DownloadAttempts) {
    # Set $filename
    If ($DestinationFileName) { $filename = $DestinationFileName }
    else { $filename = $URL.Split('/')[-1] }
    # Create random BITS Transfer ID
    $id = (-join ((48..57) + (97..122) | Get-Random -Count 8 | ForEach-Object { [char]$_} ))
    # Initiate transfer - With credentails from Nordlo dist server
    if ($Credentials -and ($URL -like "*dist.*.nordlo.cloud*")) {
      Write-NELog -Message "Initiating download of file $filename (using credentials)"
      Write-NELog -Message "using supplied credentials - attempt $a of $DownloadAttempts"
      # With / without DestinationFileName specified
      if ($DestinationFileName) { $null = Start-BitsTransfer -Authentication Basic -Credential $Credentials -Asynchronous -DisplayName $id -Source $URL -Destination "$global:WorkingFolder\$DestinationFileName" }
      else { $null = Start-BitsTransfer -Authentication Basic -Credential $Credentials -Asynchronous -DisplayName $id -Source $URL -Destination $global:WorkingFolder }
    }
    # Without credentials
    else {
      Write-NELog -Message "Initiating download of file $filename (not using credentials)"
      Write-NELog -Message "No credentials specified - attempt $a of $DownloadAttempts"
      # With / without DestinationFileName specified
      if ($DestinationFileName) { $null = Start-BitsTransfer -Asynchronous -DisplayName $id -Source $URL -Destination "$global:WorkingFolder\$DestinationFileName" }
      else { $null = Start-BitsTransfer -Asynchronous -DisplayName $id -Source $URL -Destination "$global:WorkingFolder" }
    }
    # Wait for transfer to complete
    while (((Get-BitsTransfer | Where-Object {($_.DisplayName -eq $id)}).JobState -eq "Connecting") -or ((Get-BitsTransfer | Where-Object {($_.DisplayName -eq $id)}).JobState -eq "Transferring")) { Start-Sleep -Seconds 1 }
    # Get finish time
    $finishtime = Get-Date
    # Get result before completing
    #$result = (Get-BitsTransfer | Where-Object {($_.DisplayName -eq $id)}).InternalErrorCode
    $result = Get-BitsTransfer | Where-Object {$_.DisplayName -eq $id} | Select-Object *
    # Complete transfer
    Get-BitsTransfer | Complete-BitsTransfer
    # Report successfull download and write stats.
    If ($result.InternalErrorCode -eq 0) {
      # Get file size in appropriate unit
      switch ($result.BytesTransferred) {
        {$_ -ge 0 -and $_ -le 1024} { $transferSize = $_ ; $transferUnit = 'B' }
        {$_ -ge 1025 -and $_ -le 1024000} { $transferSize = [math]::Round($_ / 1KB ,2) ; $transferUnit = 'KB' }
        {$_ -ge 1024001 -and $_ -le 1024000000} { $transferSize = [math]::Round($_ / 1MB ,2) ; $transferUnit = 'MB'}
        {$_ -ge 1024000001} { $transferSize = [math]::Round($_ / 1GB ,2) ; $transferUnit = 'GB'}
      }
      # Get transfer time in seconds
      $transfertime = (New-TimeSpan -Start $result.CreationTime -End $finishtime).Seconds
      # Get transfer speed
      $transferspeeed = [math]::Round(($result.BytesTransferred / $transfertime)/ 1MB ,2)
      Write-NELog "Download succeeded on attempt $a"
      Write-NELog "Transfer statistics"
      Write-NELog " File size: $transferSize $transferUnit"
      Write-NELog " Transfer time: $transfertime second(s)"
      Write-NELog " Average speed: $transferspeeed MB/s"
      # Wait $VerifySeconds seconds and check if file still exists
      Start-Sleep -Seconds $VerifySeconds
      If (Test-Path -Path "$global:WorkingFolder\$filename") { Add-NEExitCode -ExitCode 0 ; Write-NELog -Message " File remains after $VerifySeconds seconds" }
      else { Add-NEExitCode -ExitCode 1 ; Write-NELog -Message "File missing after $VerifySeconds seconds. Check Anti Virus / Anti Malware!" -LogLevel 3}
    }
    # If download failed, report unsuccessful attempt and wait 5-10 seconds, if not last attempt
    else {
      Write-NELog "Download attempt $a failed"
      # If download attempts remains, Increment attempt counter by 1 and wait 5-10 seconds
      If ($a -lt $DownloadAttempts) {
        $a = $a + 1
        Start-sleep -Seconds (5..10 | get-random)
      }
      # If no download attempt remains, Add Exit Code 1, Write red message and Invoke-FinalAction
      else {
        Add-NEExitCode -ExitCode 1 ; Write-NELog -Message "Download FAILED - Exiting script!" -LogLevel 3
        Invoke-FinalAction
      }
    }
  }
}


Function Invoke-NEWIMoperations {
  [CmdletBinding()]
  Param(
      [Parameter(Mandatory=$true)] [string]$WIMfile
  )
  # Create mount directory if missing
  $m = if (!(Test-Path "$global:WorkingFolder\mount")) { New-Item -ItemType Directory -Path "$global:WorkingFolder\mount" }
  # Mount wim file
  Write-NELog -Message "Mount $WIMfile to $global:WorkingFolder\mount"
  $m = Mount-WindowsImage -ImagePath $global:WorkingFolder\$WIMfile -Index 1 -Path $global:WorkingFolder\Mount
  # Check result
  $m =  Get-ChildItem -Path "HKLM:\SOFTWARE\Microsoft\WIMMount\Mounted Images" | Get-ItemProperty | Where-Object "WIM Path" -eq $global:WorkingFolder\$WIMfile
  if ($m.Status) { 
      Write-NELog -Message "$WIMfile was mounted to $global:WorkingFolder\mount"
      Add-NEExitCode -ExitCode 0
  }
  else { 
    Write-NELog -Message "Mount FAILED" -LogLevel 3
    Add-NEExitCode -ExitCode 1
  }
}


# Function Install-WingetApplication - Install Winget application by ID
function Install-NEWingetApplication {
  param (
    [Parameter(Mandatory=$true)] [string]$Id
  )
  # Make sure Winget is installed and correct min version
  Invoke-WingetInstallOrUpdate
  # Resolve winget.exe path
  $WingetBin = Resolve-Path "C:\Program Files\WindowsApps\Microsoft.DesktopAppInstaller_*_*__8wekyb3d8bbwe\winget.exe"
  # Check if application matching id is installed
  $WingetLookup = (& $WingetBin list --id $Id --accept-source-agreements) | Select-Object -Last 1
  # Start installation if application is not found on system
  if (!($WingetLookup.Contains($Id))) {
    $WingetLogFile = "C:\Logs\" +$ID + "-install-" + (Get-Date -Format "yyyyMMdd_HHmmss") + ".log"
    Write-NELog -Message "Installing Winget package $Id"
    $i = Start-Process -FilePath $WingetBin -ArgumentList "install --exact --id $Id --Log $WingetLogFile --accept-source-agreements --accept-package-agreements --silent" -Wait -PassThru
    Invoke-InstallResult -ExitCode $i.Exitcode
    if (Test-Path -Path $WingetLogFile) { Write-NELog -Message "See $WingetLogFile for details" }
  }
  else { Write-NELog -Message "$Id already installed on system" -LogLevel 2 }
}


# Function Uninstall-WingetApplication - uninstall Winget application by ID
function Uninstall-NEWingetApplication {
  param (
    [Parameter(Mandatory=$true)] [string]$Id
  )
  # Make sure Winget is installed and correct min version
  Invoke-WingetInstallOrUpdate
  # Resolve winget.exe path
  $WingetBin = Resolve-Path "C:\Program Files\WindowsApps\Microsoft.DesktopAppInstaller_*_*__8wekyb3d8bbwe\winget.exe"
  # Check if application matching id is installed
  $WingetLookup = (& $WingetBin list --id $Id --accept-source-agreements) | Select-Object -Last 1
  # Start Uninstallation if application is found on system
  if ($WingetLookup.Contains($Id)) {
    $WingetLogFile = "C:\Logs\" +$ID + "-uninstall-" + (Get-Date -Format "yyyyMMdd_HHmmss") + ".log"
    Write-NELog -Message "Uninstalling Winget package $Id"
    $u = Start-Process -FilePath $WingetBin -ArgumentList "uninstall --exact --id $Id --Log $WingetLogFile --accept-source-agreements --silent" -Wait -PassThru
    Invoke-InstallResult -ExitCode $u.Exitcode
    if (Test-Path -Path $WingetLogFile) { Write-NELog -Message "See $WingetLogFile for details" }
  }
  else { Write-NELog -Message "$Id not found on system" -LogLevel 2 }
}


# Function Invoke-WingetInstallOrUpdate - Install or update Winget to min version. Called by functions Install-WingetApplication and Uninstall-WingetApplication
function Invoke-NEWingetInstallOrUpdate {
  param ()

  # Winget URLs and settings
  $Winget_Microsoft_Desktop_Installer = "https://aka.ms/getwinget"
  $Winget_Microsoft_VC_Libs_x64 = "https://aka.ms/Microsoft.VCLibs.x64.14.00.Desktop.appx"
  $Winget_Microsoft_UI_Xaml = "https://github.com/microsoft/microsoft-ui-xaml/releases/download/v2.8.5/Microsoft.UI.Xaml.2.8.x64.appx"
  $Winget_MinVer = "1.6"
  # Resolve winget.exe path and get version if exists
  $WingetBin = (Resolve-Path "C:\Program Files\WindowsApps\Microsoft.DesktopAppInstaller_*_*__8wekyb3d8bbwe\winget.exe").Path
  if ($WingetBin) { $WingetVersion = ((& $WingetBin --version)).substring(1) }
  # Install Winget if missing or too old
  if (!($WingetBin) -or $WingetVersion -lt $Winget_MinVer) {
    Write-NELog -Message "Winget missing or too old - Installing latest version"
    Invoke-FileTransfer -URL $Winget_Microsoft_VC_Libs_x64 -DestinationFileName "Microsoft.VCLibs.x64.14.00.Desktop.appx"
    Invoke-FileTransfer -URL $Winget_Microsoft_UI_Xaml -DestinationFileName "Microsoft.UI.Xaml.x64.appx"
    Invoke-FileTransfer -URL $Winget_Microsoft_Desktop_Installer -DestinationFileName "Microsoft.DesktopAppInstaller_8wekyb3d8bbwe.msixbundle"
    $null = Add-AppxProvisionedPackage -Online -PackagePath "$global:WorkingFolder\Microsoft.VCLibs.x64.14.00.Desktop.appx" -SkipLicense
    $null = Add-AppxProvisionedPackage -Online -PackagePath "$global:WorkingFolder\Microsoft.UI.Xaml.x64.appx" -SkipLicense
    $null = Add-AppxProvisionedPackage -Online -PackagePath "$global:WorkingFolder\Microsoft.DesktopAppInstaller_8wekyb3d8bbwe.msixbundle" -SkipLicense
    Start-Sleep -Seconds 10
    # Proceed if update was successful, abort if not
    $WingetBin = (Resolve-Path "C:\Program Files\WindowsApps\Microsoft.DesktopAppInstaller_*_*__8wekyb3d8bbwe\winget.exe").Path
    $WingetVersion = ((& $WingetBin --version)).substring(1)
    if ($WingetVersion -ge $Winget_MinVer ) { Write-NELog -Message "Winget was successfully installed/updated" }
    else {
      Invoke-InstallResult -ExitCode 1
      Write-NELog -Message "Winget could not be updated to minimal version ($Winget_MinVer) - exiting" -LogLevel 3
      Invoke-FinalAction
    }
  }
  else { Write-NELog -Message "A supported version of Winget was found" }
}


# Function Invoke-NERemovePublicDesktopShortcuts - Removes shortcuts on public desktop added during install session
function Invoke-NERemovePublicDesktopShortcuts {
  param ()
  # Get list of current public desktop shortcuts
  $ShortcutsPost = (Get-ChildItem -Path "C:\Users\Public\Desktop" -Filter "*.lnk").FullName
  # If public desktop contained shortcuts pre installation
  if ($ShortcutsPre) {
    # Compile list of new shortcuts
    $ShortcutsNew = Compare-Object -ReferenceObject $script:ShortcutsPre -DifferenceObject $ShortcutsPost | Where-Object { $_.SideIndicator -eq '=>' } | ForEach-Object { $_.InputObject }
    # Remove new shortcuts
    if ($ShortcutsNew) {
      $ShortcutsNew | ForEach-Object {
        if (Test-Path -Path $_) { 
          Write-NELog -Message "Removing shortcut $($_)"
          Remove-Item -Path $_
        }
      }
    }
    else {
      Write-NELog -Message "No new shortcuts found on public desktop"
    }
  }
  # If public desktop did not contain shortcuts pre installation
  else {
    if ($ShortcutsPost) {
      $ShortcutsPost | ForEach-Object {
        if (Test-Path -Path $_) { 
          Write-NELog -Message "Removing shortcut $($_)"
          Remove-Item -Path $_
        }
      }
    }
    else {
      Write-NELog -Message "No new shortcuts found on public desktop"
    }
  }
}


Function Edit-NEIniFileContent {
  [CmdletBinding()]
  Param(
    [Parameter(Mandatory=$true)] [string]$IniFile,
    [Parameter(Mandatory=$true)] [String]$Content,
    [Parameter(Mandatory=$true)] [String]$Section,
    [Parameter(Mandatory=$true)] [String]$Value
  )
  
  if (Test-Path -Path $IniFile) {
    # Install NuGet package manager if missing
    try {
      $packages = (Get-PackageProvider).name
      if (!($packages -like ("NuGet"))) {
      Install-PackageProvider -name NuGet -Force
      }
      Import-Module PsIni -Force
    }
    catch {
      Install-Module -Scope CurrentUser PsIni -Force
      Import-Module PsIni -Force
    }
    if ( $Content -ne (Get-IniContent $IniFile)[$Section][$Value]) {
      $ini = Get-IniContent $IniFile
      $ini[$Section][$Value] = $Content
      $ini | Out-IniFile -Force -Encoding Unicode -FilePath $IniFile
      Write-NELog -Message "INI file $IniFile content has been added or modified"
      Invoke-InstallResult -ExitCode 0
    }
    else {
      Write-NELog -Message "Content of $IniFile is was up to date, no changees were made" -LogLevel 2
      Invoke-InstallResult -ExitCode 0
    }
  }
  else {
    Write-NELog -Message "$IniFile was not found" -LogLevel 3
    Invoke-InstallResult -ExitCode 1
  }
}


Function Add-NEExitCode {
  [CmdletBinding()]
  Param (
      [Parameter(Mandatory=$true)] [INT]$ExitCode,
      [Parameter(Mandatory=$false)] [INT]$RebootRequired
  )
  # set $script:DeployExitCode = 1 if not exist prior, else add to existing
  if (!($script:DeployExitCode)) { $script:DeployExitCode = $ExitCode }
  else { $script:DeployExitCode +=$ExitCode }
  # set $script:RebootRequired
  if ($RebootRequired -eq 1) { $script:RebootRequired = 1 }
}


Function  Invoke-NEInstallResult {
  [CmdletBinding()]
  Param (
    [Parameter(Mandatory=$true)] [string]$ExitCode
  )
  # Exit code 0 - The action completed successfully (ERROR_SUCCESS)
  If (($ExitCode -eq 0) -or ($ExitCode -eq 0x80070000 )) {
    Add-NEExitCode -ExitCode 0
    Write-NELog -Message "Operation Succeeded 0"
  }
  # Exit code 1707 - Installation operation completed successfully (ERROR_SUCCESS)
  elseif (($ExitCode -eq 1707) -or ($ExitCode -eq 0x800706ab )) {
    Add-NCExitCode -ExitCode 0
    Write-NCLog -Message "Operation Succeeded (1707)"
  }
  # Exit code 3010 - restart is required to complete the install (ERROR_SUCCESS_REBOOT_REQUIRED)
  elseIf (($ExitCode -eq 3010) -or ($ExitCode -eq 0x80070bc2 )) {
    Add-NEExitCode -ExitCode 0 -RebootRequired 1
    Write-NELog -Message "Operation Succeeded 3010 - reboot required" -LogLevel 1
  }
  # Exit code 1638 (999) - Another version of this product is already installed (ERROR_PRODUCT_VERSION)
  elseIf (($ExitCode -eq 1638) -or ($ExitCode -eq 666) -or ($ExitCode -eq 0x80070666 )) {
    Add-NEExitCode -ExitCode 0
    Write-NELog -Message "Operation Succeeded $ExitCode - product already installed" -LogLevel 2
  }
  # Exit code 1605 (645) - This action is only valid for products that are currently installed. (ERROR_UNKNOWN_PRODUCT)
  elseIf (($ExitCode -eq 1605) -or ($ExitCode -eq 645) -or ($ExitCode -eq 0x80070645 )) {
    Add-NEExitCode -ExitCode 0
    Write-NELog -Message "Operation Succeeded $ExitCode - product not installed on system" -LogLevel 2
  }
  # Exit code anything else
  else { 
    Add-NEExitCode -ExitCode 1
    Write-NELog -Message "Operation FAILED $ExitCode" -LogLevel 3
  }
  Start-Sleep -Seconds 2
}


Function Invoke-NEResetDeployEnvironment {
  # Dismount wim image if mounted
  if ((Test-Path -Path "HKLM:\SOFTWARE\Microsoft\WIMMount\Mounted Images") -and (Get-ChildItem -Path "HKLM:\SOFTWARE\Microsoft\WIMMount\Mounted Images" | Get-ItemProperty | Where-Object "Mount Path" -eq "$global:WorkingFolder\Mount")) {
    if ((Get-ChildItem -Path "HKLM:\SOFTWARE\Microsoft\WIMMount\Mounted Images" | Get-ItemProperty | Where-Object "Mount Path" -eq "$global:WorkingFolder\Mount").Status) {
      Write-NELog -Message "Mounted Windows images detected (WIM file mounted)"
      # Close any open explorer windows
      Write-NELog -Message "Close any Explorer windows ($global:WorkingFolder\Mount)"
      $shell = New-Object -ComObject Shell.Application
      $window = $shell.Windows() | Where-Object { $_.LocationURL -like "$(([uri]"$global:WorkingFolder\Mount").AbsoluteUri)*" }
      $window | ForEach-Object { $_.Quit() }
      # Close any running processes
      Write-NELog -Message "Close any runnig processes ($global:WorkingFolder\Mount)"
      get-process | Where-Object { $_.Path -like "$global:WorkingFolder\Mount\*" } | ForEach-Object { Stop-Process -Name $_.Name -Force -ErrorAction SilentlyContinue }
      Start-Sleep -Seconds 5
      # Dismount image
      Write-NELog -Message "Dismount $global:WorkingFolder\Mount"
      $m = Dismount-WindowsImage -Path "$global:WorkingFolder\Mount" -Discard
      Start-Sleep -Seconds 5
      $m =  Get-ChildItem -Path "HKLM:\SOFTWARE\Microsoft\WIMMount\Mounted Images" | Get-ItemProperty | Where-Object "WIM Path" -eq $global:WorkingFolder\$WIMfile
      if (!($m.Status)) {
        Write-NELog "Windows image successfully dismounted."
        Remove-Item -Path $global:WorkingFolder\Mount -Force
        $script:WimStatus = $true
        }
      else { 
        Write-NELog -Message "Windows image dismount FAILED." -LogLevel 3
        $script:WimStatus = $false
        }
    }
  }
  else {
    Write-NELog -Message "No mounted Windows images found (ok)"
    $script:WimStatus = $true
    }

  # Remove all AppDeploy* variables
  Write-NELog -Message "Remove AppDeploy* variables"
  Get-Variable | Where-Object { $_.Name -Like "*AppDeploy*" } | Remove-Variable -ErrorAction SilentlyContinue
}


Function Invoke-NEFinalAction {
  Write-NELog -Message " "
  Write-NELog -Message "### Final Actions"
  # Remove public desktop shortcuts created during session if $Global:PublicDesktopShortcuts=Remove
  if ($Global:PublicDesktopShortcuts -eq "Remove") { Invoke-NERemovePublicDesktopShortcuts }
  # Reset environment - Dismount any mounted Windows Images (WIM) files
  Invoke-NEResetDeployEnvironment
  # Remove temporary folder if $script:WimStatus is $true
  if ( $script:WimStatus = $true ) {
    # Close any open explorer windows
    Write-NELog -Message "Close any Explorer windows ($global:WorkingFolder)"
    $shell = New-Object -ComObject Shell.Application
    $window = $shell.Windows() | Where-Object { $_.LocationURL -like "$(([uri]"$global:WorkingFolder").AbsoluteUri)*" }
    $window | ForEach-Object { $_.Quit() }
    # Close any running processes
    Write-NELog -Message "Close any runnig processes ($global:WorkingFolder)"
    get-process | Where-Object { $_.Path -like "$global:WorkingFolder\*" } | ForEach-Object { Stop-Process -Name $_.Name -Force -ErrorAction SilentlyContinue }
    Write-NELog -Message "Remove temporary folder $global:WorkingFolder"
    Remove-Item $global:WorkingFolder -Recurse -Force
    if (!(Test-Path -Path $global:WorkingFolder)) { Write-NELog "Temporary folder was successfully removed" }
    else { Write-NELog -Message "FAILURE - Temorary folder could not be removed." -LogLevel 3 }
  }
  else { Write-NELog -Message "FAILURE - Windows Image dismount FAILED. Wim mount, temporary folder and files will remain on computer and must be removed manually." -LogLevel 3 }
  # Remove logon maintenance message
  Write-NELog "Removing logon message"
  $null = New-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" -Name "LegalNoticeCaption" -PropertyType String -Value "" -Force
  $null = New-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" -Name "LegalNoticeText" -PropertyType String -Value "" -Force
  # Remove files used by Invoke-NotificationSchedule
  
  if (Test-Path -Path "$AppInstallLogDir\NordloGreen.jpg") { Remove-Item -Path "$AppInstallLogDir\NordloGreen.jpg" -Force }
  if (Test-Path -Path "$AppInstallLogDir\NotificationForm.ps1") { Remove-Item -Path "$AppInstallLogDir\NotificationForm.ps1" -Force }
  # Exit with "3010" if $script:DeployExitCode is 0 and $script:RebootRequired is 1
  if ( ($script:DeployExitCode -eq 0) -and ($script:RebootRequired -eq 1) ){
    Write-NELog -Message " "
    Write-NELog -Message "Final status: Success - reboot required (3010)"
    Start-Sleep -Seconds 1
    Exit 3010
  }
  # Exit with "0" if exit code is missing or $script:DeployExitCode is 0 and $script:RebootRequired is not 1 (or missing)
  elseif ( (!($script:DeployExitCode)) -or ($script:DeployExitCode -eq 0))  {
    Write-NELog -Message " "
    Write-NELog -Message "Final status: SUCCESS (0)"
    Start-Sleep -Seconds 1
    Exit 0
  }
  # Exit with "1" if $script:DeployExitCode is > 0
  else {
    Write-NELog -Message " "
    Write-NELog  -Message "Final status: FAILURE (1)" -LogLevel 3
    Start-Sleep -Seconds 6
    Exit 1
  }
}