<#
.DESCRIPTION
	Tools, die für die Onboarding-Automatisation benützt werden
	Automatische Vorbereitung der Konfig:
	- C#1
		Wenn $Password definiert ist
		Wird es $SecurePassword oder daraus ein SecureString erzeugt
	- C#2
		Allenfalls die NPID ergänzen und $NpidFqdn erzeugen

.PARAMETER GetHelp
	Das Script zeigt die kurze Übersicht der Hilfe an

.PARAMETER GetHelpCls
	Das Script löscht den Bildschirm und zeigt die kurze Hilfe an

.PARAMETER GetEx
	Das Script kopiert das gewünschte Beispiel ins Clipboard

.PARAMETER RunEx
	Das Script führt das gewünschte Beispiel direkt aus

.EXAMPLE
	. c:\Temp\NoserOnboarding\Tom-Tools.ps1

	Lädt die Script-Fuktionen in die aktuelle Shell, damit die Befehle genützt werden können

.EXAMPLE
	$UninstallCiscoSW_Whitelist
	Die Whitelist der Cisco-SW, die zur Vorbereitung des Onboardings nicht deinstalliert wird

.EXAMPLE
	Get-CiscoSW-ToUninstall -Property DisplayVersion, VersionMajor, Installdate, Uninstallstring
	Listet alle Cisco-SW, die zur Vorbereitung des Onboardings deinstalliert würde
	Filtert Software, die in dieser Whitelist ist: $UninstallCiscoSW_Whitelist

.EXAMPLE
	Get-Installed-Software -Property DisplayVersion,VersionMajor,Installdate,Uninstallstring -IncludeProgram '*Cisco *' | Select ProgramName
	Listet alle installierte SW aus, die 'Cisco' im Namen hat

.EXAMPLE
	Print-Computer-Info
	Gibt Informationen zum Computer aus, um ein IT-Ticket zu machen

.EXAMPLE
	Get-NetworkConfig
	Liefert die MAC-Adresse, den Namen und die IP-Adresse der NetworkAdapter

.EXAMPLE
	Get-UserNosergroupLan-Certificates
	Liefert die User-Zertifikate der Nosergruppe

.EXAMPLE
	Get-User-NPID
	Liefert von den User-Zertifikaten der Nosergruppe die NPID

.EXAMPLE
	Get-Phonetic-Password
	Erzeugt ein phonetisches Passwort

.NOTES
	001, 200806, tom-agplv3@jig.ch
	002, 200819
		wget
			Fehler korrigiert: The response content cannot be parsed because the Internet Explorer engine is not available
				Mit: -UseBasicParsing
			Neu mti Timeout
				$WgetTimeoutSec
	003, 201013
		Zugefügt: Get-Help 
	004, 201126
		Neue Funktionen:
			Get-UserNosergroupLan-Certificates
			Get-User-NPID
	004, 210120
		SendWait: Tastendrücke von Daten und die abschliessenden Steuertasten werden separat geschickt
	005, 210423
		eNoserFirma: Ergänzt um Frox
		Neu: $ZertifikatVorlageDropDownCfg = @{}
	006, 210630
		eNoserFirma: Ergänzt um Nyp
		Stop-Process: neu mit -force, 
			weil sonst passiert, dass der User nach der Bestätigung gefragt wird
	007, 211129
		Neu: Select-Anyconnect-CoreVpnPredeploy()
		Verbessert: Cisco-VPN-Connect-AndWait()
	008, 230703
		Neu: Remove-Obsolete-UserNosergroupLan-Certificates()
		Verbessert: Cisco-VPN-Connect-AndWait()
#>
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Low', DefaultParameterSetName = 'Script')]
Param (
	# Get-Help Parameter
	# 005, 200314
	[Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'GetHelp')]
	[Switch]$GetHelp = $false,
	
	[Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'GetHelp')]
	[Switch]$GetHelpCls = $false,
	
	[Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'GetHelp')]
	[int]$GetEx,
	
	[Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'GetHelp')]
	[int]$RunEx
)

# Write-Host $PsCmdlet.ParameterSetName



## Global Config
# Die aktuell verwendete Version von 
# anyconnect-win-4.10.02086-core-vpn-predeploy-k9.msi
$AnyconnectCoreVpnPredeployPreferedVersion = '4.10.02086'

# Zur Vorbereitung des Onboardings wird diese Cisco-SW nicht deinstalliert
$UninstallCiscoSW_Whitelist = @('.*Webex Teams.*', '.*Cisco Webex Productivity Tools.*', '.*Cisco Webex Meetings.*')

$NpidSuffix = '@user.nosergroup.lan'
$CiscoAnyConnect_VPNServer = 'access.nosergroup.com'
$CreateCertificatreURL = 'https://mycertificates.ise.nosergroup.com/'
$WelcomeURL = 'http://welcome.nosergroup.lan'
$ServiceDeskURL = 'https://servicedesk.nosergroup.com'

# Wie lange soll wget warten, bis ein Fehler erzeugt wird?
$WgetTimeoutSec = 5
$AnyConnect_Setup_ISE_NetworkAssistant = '*anyconnect-ise-network-assistant-win*.exe'

# Die Cisco AnyConnect exe's suchen
$CiscoVpnUiExe = Get-ChildItem ${env:ProgramFiles(x86)} -Recurse -Filter 'vpnui.exe' -ErrorAction SilentlyContinue | Select -First 1 -Expandproperty Fullname
$CiscoVpnCliExe = Get-ChildItem ${env:ProgramFiles(x86)} -Recurse -Filter 'vpncli.exe' -ErrorAction SilentlyContinue | Select -First 1 -Expandproperty Fullname


Enum eNoserFirma {
	Unknown = 1
	Akros = 2
	NoserEngineering = 3
	Frox = 4
	Nyp = 5
}


# Zertifikat-Vorlage: Die Definition der DropDown-Liste
$ZertifikatVorlageDropDownCfg = @{
	# Wenn wir die Firma nicht erkennen, springen wir nur zum nächsten Feld im Formular
	# zum Erzeugen des Zertifikats
	[eNoserFirma]::Unknown = @{
		DropDownItem = ''
		Keys = '{Tab}'
	}

	# Wird (z.Z.) nicht erkannt:
	# 'ng_ak_external_employee_certificate'
	
	# Akros-intern
	[eNoserFirma]::Akros = @{
		DropDownItem = 'ng_ak_employee_certificate'
		Keys = 'ng_ak_empl{Tab}'
	}

	[eNoserFirma]::NoserEngineering = @{
		DropDownItem = 'ng_ne_employee_certificate'
		Keys = 'ng_ne_empl{Tab}'
	}
	
	[eNoserFirma]::Frox = @{
		DropDownItem = 'ng_fx_employee_certificate'
		Keys = 'ng_fx_empl{Tab}'
	}

	[eNoserFirma]::Nyp = @{
		DropDownItem = 'ng_ny_employee_certificate'
		Keys = 'ng_ny_empl{Tab}'
	}

	# Wird (z.Z.) nicht erkannt:
	# 'EAP_Authentication_Certificate_Template'
	# 'ng_nm_employee_certificate'
	# 'ng_dx_employee_certificate'
	# 'ng_bs_employee_certificate'
}



#Region Toms Tools: Get-Help
# 005, 200314

# Liefert $True, wenn $TestString vermutlich ein PowerShell Script / Kommando ist,
# sonst ist es vermutlich ein normaler Text
Function Is-PowerShell-Command($TestString) {
	$Token = $Null
	$ParseError = $Null
	
	$Res = [System.Management.Automation.Language.Parser]::ParseInput($TestString, [ref]$Token, [ref]$ParseError)
	
	# Wenn wir anderes als nur Tokens haben, haben wir vermutlich ein Script
	If (($Token | ? { ($_.GetType()).Name -ne 'Token' }).Count -gt 0) {
		# Vermutlich ein Script
		$True
	} Else {
		# Vermutlich Text
		$False
	}
}


# Zeigt eine nützliche Formatierung von Get-Help -Examples 
Function Show-Help($ScriptFullName, $HelpText, $RecognizeCode = $false) {
	$ScriptFileName = [IO.Path]::GetFileName($ScriptFullName)
	
	# Die Synopsis (den Header) anzeigen
	Write-Host $HelpText.Synopsis -ForegroundColor Green
	Write-Host ''
	
	$ExNo = 1
	ForEach ($Example In $HelpText.Examples.Example) {
		# Jedes Beispiel anzeigen
		# Den Titel
		Write-Host ("Example #{0}" -f ($ExNo++)) -ForegroundColor Cyan
		# Den Code
		Write-Host ($Example.Code.Replace(".\$ScriptFileName", $ScriptFullName)) -ForegroundColor Yellow
		
		# Die Beschreibung
		# Die Leeren Zeilen am Ende löschen
		$Comments = $Example.Remarks.Text -replace "(?s)`r`n\s*$"
		# Die ersten Zeilen im Kommentar allenfalls als Code anzeigen
		$CommentStarted = $False
		ForEach ($Comment In $Comments) {
			# Die Comments in Zeilen aufteilen und leere Zeilen ignorieren
			$Lines = ($Comment -split "`r`n|`r|`n") | ? { -not [String]::IsNullOrEmpty($_) }
			ForEach ($Line In $Lines) {
				If ($RecognizeCode -eq $true -and $CommentStarted -eq $False -and (Is-PowerShell-Command $Line)) {
					# Der Kommentar ist vermutlich ein PowerShell Befehl
					Write-Host $Line.Replace(".\$ScriptFileName", $ScriptFullName) -ForegroundColor Yellow
				} Else {
					# Der Kommentar ist vermutlich Text
					If ($CommentStarted -eq $False) {
						$CommentStarted = $True
						Write-Host ''
					}
					Write-Host $Line
				}
			}
		}
		Write-Host ''
	}
}


# Schreibt eines der Examples ins ClipBoard
# oder liefert @($ExampleIsExecutable, $ScriptFullName, $Parameters)
Function Get-Ex($GetEx, $ScriptFullName, $HelpText, $ToClipboard = $True) {
	$ScriptFileName = [IO.path]::GetFileName($ScriptFullName)
	$Example = $HelpText.Examples.Example[($GetEx - 1)]
	# Wenn das Beispiel mit .\.\ beginnt, kann es auch direkt ausgeführt werden
	$ExampleIsExecutable = $Example.Code.StartsWith('.\.\')
	If ($ExampleIsExecutable) {
		$Parameters = $Example.Code.Replace(".\.\$($ScriptFileName)", '').Trim()
	} Else {
		$Parameters = $Example.Code.Replace(".\$($ScriptFileName)", '').Trim()
	}
	
	If ($ToClipboard) {
		Set-Clipboard -Value ("{0} {1}" -f $ScriptFullName, $Parameters)
	} Else {
		Return @($ExampleIsExecutable, $ScriptFullName, $Parameters)
	}
}

If ($GetHelp -or $GetHelpCls) {
	If ($GetHelpCls) { CLS }
	Show-Help $MyInvocation.InvocationName (Get-Help -Examples $MyInvocation.InvocationName)
	Break Script
}

# Kopiert eines der Examples ins Clipboard
If ($GetEx) {
	Get-Ex $GetEx $MyInvocation.InvocationName (Get-Help -Examples $MyInvocation.InvocationName)
	Break Script
}

# Führt eines der Examples direkt aus
If ($RunEx) {
	$ExampleIsExecutable, $ScriptFullName, $Parameters = Get-Ex $RunEx $MyInvocation.InvocationName (Get-Help -Examples $MyInvocation.InvocationName) -ToClipboard:$False
	If ($ExampleIsExecutable) {
		$Command = ("{0} {1}" -f $ScriptFullName, $Parameters)
		Invoke-Expression -Command $Command
	} Else {
		Write-Host "Example $RunEx kann nicht direkt ausgeführt werden - es wurde in die Zwischenablage kopiert"
		Set-Clipboard -Value ("{0} {1}" -f $ScriptFullName, $Parameters)
	}
	Break Script
}

#Endregion Toms Tools: Get-Help



# Ist vermutlich das Verzeichnis dieses Tom-Tools.ps1 Files
Function Get-ScriptDir {
	"{0}\" -f $($MyInvocation.PSScriptRoot)
}


# 200806
Function Has-Value($Data) {
	If ($Data -eq $null) { Return $False }
	Switch ($Data.GetType().Name) {
		'String' {
			If ([String]::IsNullOrEmpty($Data)) { Return $False } 
			Else { Return $True }
		}
		Default {
			Return $True
		}
	}
}


#Region Toms Tools: Log
# Log
# Prüft, ob $Script:LogColors definiert ist und nützt dann dieses zur Farbgebung
# $Script:LogColors =@('Cyan', 'Yellow')
# 
# 0: Thema - 1: Kapitel - 2: OK - 3: Error
# 200604 175016
# 200805 103305
# Neu: Optional BackgroundColor
# 211129 110213
# Fix -ClrToEol zusammen mit -ReplaceLine 
$Script:LogColors = @('Green', 'Yellow', 'Cyan', 'Red')
Function Log() {
	[CmdletBinding(SupportsShouldProcess)]
	Param (
		[Parameter(Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
		[Int]$Indent,
		
		[Parameter(Position = 1, ValueFromPipeline, ValueFromPipelineByPropertyName)]
		[String]$Message = '',
		
		[Parameter(Position = 2, ValueFromPipeline, ValueFromPipelineByPropertyName)]
		[ConsoleColor]$ForegroundColor,
		
		# Vor der Nachricht eine Leerzeile
		[Parameter(Position = 3, ValueFromPipeline, ValueFromPipelineByPropertyName)]
		[Switch]$NewLineBefore,
		
		# True: Die aktuelle Zeile wird gelöscht und neu geschrieben
		[Parameter(Position = 4, ValueFromPipeline, ValueFromPipelineByPropertyName)]
		[Switch]$ReplaceLine = $false,
		
		# True: Am eine keinen Zeilenumbruch
		[Parameter(Position = 5, ValueFromPipeline, ValueFromPipelineByPropertyName)]
		[Switch]$NoNewline = $false,
		
		# Append, also kein Präfix mit Ident
		[Parameter(Position = 6, ValueFromPipeline, ValueFromPipelineByPropertyName)]
		[Switch]$Append = $false,
		
		# Löscht die Zeile bis zum Zeilenende
		[Parameter(Position = 7, ValueFromPipeline, ValueFromPipelineByPropertyName)]
		[Switch]$ClrToEol = $false,
		
		[Parameter(Position = 8, ValueFromPipeline, ValueFromPipelineByPropertyName)]
		[ConsoleColor]$BackgroundColor
	)
	
	If ($Script:LogDisabled -eq $true) { Return }
	If ($Script:DefaultBackgroundColor -eq $null) { $Script:DefaultBackgroundColor = (Get-Host).UI.RawUI.BackgroundColor }
	If ([String]::IsNullOrEmpty($Message)) { $Message = '' }

	If ($Indent -eq $null) { $Indent = 0 }
	If ($BackgroundColor -eq $null) { $BackgroundColor = $Script:DefaultBackgroundColor }
	
	If ($ReplaceLine) { $Message = "`r$Message" }
	
	If ($NewLineBefore) { Write-Host '' }
	
	$WriteHostArgs = @{ }
	If ($ForegroundColor -eq $null) {
		If ($Script:LogColors -ne $null -and $Indent -le $Script:LogColors.Count -and $Script:LogColors[$Indent] -ne $null) {
			Try {
				$ForegroundColor = $Script:LogColors[$Indent]
			} Catch {
				Write-Host "Ungültige Farbe: $($Script:LogColors[$Indent])" -ForegroundColor Red
			}
		}
		If ($ForegroundColor -eq $null) {
			$ForegroundColor = [ConsoleColor]::White
		}
	}
	If ($ForegroundColor) {
		$WriteHostArgs += @{ ForegroundColor = $ForegroundColor }
	}
	$WriteHostArgs += @{ BackgroundColor = $BackgroundColor }
	
	If ($NoNewline) {
		$WriteHostArgs += @{ NoNewline = $true }
	}
	
	If ($Append) {
		$Msg = $Message
		If ($ClrToEol) {
			$Width = (get-host).UI.RawUI.MaxWindowSize.Width
			If ($Msg.Length -lt $Width) {
				$Spaces = $Width - $Msg.Length
				$Msg = "$Msg$(' ' * $Spaces)"
			}
		}
	} Else {
		Switch ($Indent) {
			0 {
				$Msg = "* $Message"
				If ($NoNewline -and $ClrToEol) {
					$Width = (get-host).UI.RawUI.MaxWindowSize.Width
					If ($Msg.Length -lt $Width) {
						$Spaces = $Width - $Msg.Length
						$Msg = "$Msg$(' ' * $Spaces)"
					}
				}
				If (!($ReplaceLine)) {
					$Msg = "`n$Msg"
				}
			}
			Default {
				$Msg = $(' ' * ($Indent * 2) + $Message)
				If ($NoNewline -and $ClrToEol) {
					# Rest der Zeile mit Leerzeichen überschreiben
					$Width = (get-host).UI.RawUI.MaxWindowSize.Width
					If ($Msg.Length -lt $Width) {
						$Spaces = $Width - $Msg.Length
						$Msg = "$Msg$(' ' * $Spaces)"
					}
				}
			}
		}
	}
	
	Write-Host $Msg @WriteHostArgs
	
	# if (!([String]::IsNullOrEmpty($LogFile))) { 
	# 	"$([DateTime]::Now.ToShortDateString()) $([DateTime]::Now.ToLongTimeString())   $Message" | Out-File $LogFile -Append
	# }
}


#Endregion Toms Tools: Log


#Region Win32 Tools

Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue

If (![bool]('ApiFuncs' -as [Type])) {
	Add-Type  @"
		// https://www.pinvoke.net/index.aspx
		using System;
		using System.Runtime.InteropServices;
		using System.Text;
		public class ApiFuncs {
			[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
			public static extern Int32 GetWindowText(IntPtr hwnd, StringBuilder lpString, Int32 cch);

			// Retrieves the handle to the foreground window
			[DllImport("user32.dll", SetLastError=true, CharSet=CharSet.Auto)]
			public static extern IntPtr GetForegroundWindow();
			
			[DllImport("user32.dll", SetLastError=true, CharSet=CharSet.Auto)]
			public static extern Int32 GetWindowThreadProcessId(IntPtr hWnd, out Int32 lpdwProcessId);

			[DllImport("user32.dll", SetLastError=true, CharSet=CharSet.Auto)]
			public static extern Int32 GetWindowTextLength(IntPtr hWnd);

			[DllImport("user32.dll")]
			[return: MarshalAs(UnmanagedType.Bool)]
			public static extern bool SetForegroundWindow(IntPtr hWnd);		
			
			[DllImport("user32.dll")]
			public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int y, int cx, int cy, uint uFlags);

			public static readonly IntPtr HWND_BOTTOM = new IntPtr(1);
			public const uint SWP_NOSIZE = 0x0001;
			public const uint SWP_NOMOVE = 0x0002;			
			
			// FORCEMINIMIZE = 11
			// HIDE            = 0
			// MAXIMIZE        = 3
			// MINIMIZE        = 6
			// RESTORE         = 9
			// SHOW            = 5
			// SHOWDEFAULT     = 10
			// SHOWMAXIMIZED   = 3
			// SHOWMINIMIZED   = 2
			// SHOWMINNOACTIVE = 7
			// SHOWNA          = 8
			// SHOWNOACTIVATE  = 4
			// SHOWNORMAL      = 1
			[DllImport("user32.dll")]
			public static extern bool ShowWindowAsync(IntPtr hWnd, Int32 nCmdShow); 
			
			// more here: http://www.pinvoke.net/default.aspx/user32.showwindow
			[DllImport("user32.dll")]
			[return: MarshalAs(UnmanagedType.Bool)]
			public static extern bool ShowWindow(IntPtr hWnd, Int32 nCmdShow);			

			[DllImport("user32.dll")]
			[return: MarshalAs(UnmanagedType.Bool)]
			public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
			
			[DllImport("User32.dll")]
			public static extern bool MoveWindow(IntPtr handle, Int32 x, Int32 y, Int32 width, Int32 height, bool redraw);
			
		}

		public struct RECT
		{
			public Int32 Left;        // x position of upper-left corner
			public Int32 Top;         // y position of upper-left corner
			public Int32 Right;       // x position of lower-right corner
			public Int32 Bottom;      // y position of lower-right corner
		}
"@
} else {
	Write-Verbose 'Type already registered: ApiFuncs'
}


# Zeigt auf der Konsole alle Farbkombinationen an
Function Get-Console-Colors() {
	$colors = [enum]::GetValues([System.ConsoleColor])
	Foreach ($bgcolor in $colors){
		Foreach ($fgcolor in $colors) { Write-Host "$fgcolor|"  -ForegroundColor $fgcolor -BackgroundColor $bgcolor -NoNewLine }
		Write-Host " on $bgcolor"
	}
}


# Liefert von einer HWND die Prozess-ID
Function Get-ProcessID-From_HWND($HWND) {
	If ([String]::IsNullOrWhiteSpace($HWND)) {
		Return $Null
	}
	
	$MyPID = [IntPtr]::Zero
	$Res = [ApiFuncs]::GetWindowThreadProcessId( $HWND, [ref] $MyPID )
	# Log 4 "MyPID: $MyPID"
	Return $MyPID
}



# Verschiebt das Fenster des Prozesses in den Hintergrund
Function Send-WindowToBackground {
	Param(
		[Parameter(Mandatory, Position=0)]
		[String]$ProcessName
	)

	$Proc = Get-Process -Name $ProcessName -EA SilentlyContinue
	If ($Proc -eq $null) { Return }
	If ($Proc.MainWindowHandle -eq [IntPtr]::Zero) { Return }

	[ApiFuncs]::SetWindowPos($Proc.MainWindowHandle, [ApiFuncs]::HWND_BOTTOM, 0, 0, 0, 0, [ApiFuncs]::SWP_NOSIZE -bor [ApiFuncs]::SWP_NOMOVE)
}


# Schliesst das Applikationsfenster eines Prozesses
Function Close-ProcessWindow {
	Param(
	  [Parameter(Mandatory, Position=0)]
	  [String]$ProcessName
	)

	$Proc = Get-Process -Name $ProcessName -EA SilentlyContinue
	If ($Proc -eq $null) { Return }
	If ($Proc.MainWindowHandle -eq [IntPtr]::Zero) { Return }

	$Proc.CloseMainWindow() | Out-Null
}



# Sucht den Prozess mit einem bestimmten Titel
# !9 Wichtig!
#		Bei Chromr wird nur das zuletzt geöffnete Tab als Fenstertitel gefunden!
# !Ex
# $ProcInfo = Get-Process-FromTitle @(@('Anmelden', 'Sign On'))
Function Get-Process-FromTitle([String[]]$WindowTitles, $TimeoutSec = 999999) {
	$StartTime = Get-Date
	
	# Alle Titel suchen, beim ersten passendem Fenstertitel sind wir fertig
	Do {
		$DeltaT = New-TimeSpan -Start $StartTime -End (Get-Date)
		ForEach ($WindowTitle in $WindowTitles) {
			# Suchmuster löschen, das deifnieren wir selber
			$WindowTitle = $WindowTitle.Trim('*').Trim('.')
			$Proc = Get-Process | ? { $_.MainWindowTitle -like "*$WindowTitle*" }
			If ($Proc -ne $Null) {
				# Gefunden: Fertig
				Return $Proc
			}
		}
		# Nichts gefunden - kurz warten
		Start-Sleep -Milliseconds 500
	} While ($DeltaT.TotalSeconds -lt $TimeoutSec)
}


# Setzt ein Fenster in den Vordergrund / Activate
Function Set-Foreground-Window($MainWindowHandle) {
	# Param prüfen
	If ([String]::IsNullOrWhiteSpace($MainWindowHandle)) {
		Return
	}
	$Res = [ApiFuncs]::SetForegroundWindow($MainWindowHandle)
	$Res = [ApiFuncs]::ShowWindow($MainWindowHandle, 1)
	Start-Sleep -Milliseconds 500
}


# Liefert die $ProcessInfo, wenn $ExeFile gestartet ist 
Function Is-Exe-Running($ExeFile) {
	# Log 4 "Is-Exe-Running(): $ExeFile"
	$ExeFileName = [IO.Path]::GetFileNameWithoutExtension($ExeFile)
	$ProcessInfo = @(Get-Process -ErrorAction SilentlyContinue | ? { $_.MainWindowHandle -ne 0 -and $_.Name -eq $ExeFileName } )
	If ($ProcessInfo.Count -gt 0) {
		Return $ProcessInfo[0]
	} Else {
		Return $Null
	}
}


# Startet ein Exe im Fordergund
# Liefert die $ProcessInfo
Function Start-Exe-Foreground() {
	[CmdletBinding(SupportsShouldProcess)]
	Param (
		[String]$ExeFile,
		[Switch]$ShowInfo = $True,
		[Switch]$WaitForMainWindowHandle,
		[Switch]$TestIfAlreadyRunning = $False
	)

	If ($ShowInfo) { Log 4 "Starte: $ExeFile" }
	If ($TestIfAlreadyRunning) {
		If ($ProcessInfo = Is-Exe-Running $ExeFile) {
			If ($ShowInfo) { Log 5 'Das Programm läuft bereits' }
		} Else {
			$ProcessInfo = Start-Process -WindowStyle Normal -FilePath $ExeFile -PassThru
		}
	} Else {
		$ProcessInfo = Start-Process -WindowStyle Normal -FilePath $ExeFile -PassThru
	}

	If ($WaitForMainWindowHandle) {
		$Counter = 0
		If ($ShowInfo) { Log 4 'Warte auf das Applikations-Fenster' }
		While($ProcessInfo.MainWindowHandle -eq $Null -or $ProcessInfo.MainWindowHandle -eq 0) {
			Start-Sleep -Milliseconds 1000
			$ProcessInfo.Refresh()
			Log 3 "ProcessInfo.MainWindowHandle: $($ProcessInfo.MainWindowHandle)"
		}
	}
	Set-Foreground-Window $ProcessInfo.MainWindowHandle
	Return $ProcessInfo
}


# Wartet, bis ein exe gestartet wurde und liefert die Prozess-Info
Function Wait-For-ExeRunning($ExeFile) {
	Start-Sleep -Milliseconds 350
	$ProcessInfo = Is-Exe-Running $ExeFile
	While ($ProcessInfo -eq $null) {
		Log 3 "Warte auf den Prozess: $ExeFile"
		Start-Sleep -Milliseconds 5000
		$ProcessInfo = Is-Exe-Running $ExeFile
	}
	Return $ProcessInfo
}


# Wartet, bis ein exe gestartet wurde und liefert die Prozess-Info
Function Wait-For-ExeRunning-And-SetToForeground($ExeFile) {
	Log 2 "Aktiviere den Prozess: $ExeFile"
	$ProcessInfo = Wait-For-ExeRunning $ExeFile
	If ($ProcessInfo -ne $null) {
		Set-Foreground-Window -MainWindowHandle $ProcessInfo.MainWindowHandle
	} Else {
		Log 4 "Prozes snicht gefunden"
	}
}


# Startet ein Exe im Fordergund
Function Start-Exe-Foreground_001($ExeFile, $ShowInfo = $True) {
	If ($ShowInfo) { Log 4 "Starte: $ExeFile" }
	$ExeFileName = [IO.Path]::GetFileNameWithoutExtension($ExeFile)
	Start-Process -WindowStyle Minimized -FilePath $ExeFile
	$Counter = 0; $MainWindowHandle = 0;
	If ($ShowInfo) { Log 4 "Warte auf das Fenster" }
	While($Counter++ -lt 1000 -and $MainWindowHandle -eq 0) {
		Sleep -m 10
		$MainWindowHandle = (Get-Process $ExeFileName).MainWindowHandle
	}
	
	# if it takes more than 10 seconds then display message
	If ($MainWindowHandle -eq 0) {
		Log 4 'Could not start VPNUI it takes too long.' -ForegroundColor Red
	} Else {
		$Res = [ApiFuncs]::SetForegroundWindow($MainWindowHandle)
		$Res = [ApiFuncs]::ShowWindow($MainWindowHandle, 1)
	}
}


# Liefert den Titel des Fensters im Vordergrund
# Returns:
#  $WindowHandle, $ForegroundWindowTitle = Get-Foreground-WinTitle
Function Get-Foreground-WinTitle() {
	$WindowHandle = [ApiFuncs]::GetForegroundWindow()
	$len = [ApiFuncs]::GetWindowTextLength($WindowHandle)
	$StringBuilder = New-Object Text.StringBuilder -ArgumentList ($len + 1)
	$rtnlen = [ApiFuncs]::GetWindowText($WindowHandle, $StringBuilder, $StringBuilder.Capacity)
	$WinTitle = $StringBuilder.ToString().Trim()
	Return $WindowHandle, $WinTitle
}


# Versucht, das Vordergrundfenster zu setzen
Function Set-Foreground-WinTitle([String[]]$WindowTitles, [Int]$TimeoutSec=10) {
	
	## Haben wir bereits das richtige Fenster im Vordergrund?
	$WindowHandle, $ForegroundWindowTitle = Get-Foreground-WinTitle
	$StrFoundInTitle = $WindowTitles -contains $ForegroundWindowTitle
	$RgxFoundInTitle = String-Matches-ItemIn-ArrayWithRegex -ArrayWithRegexes $WindowTitles `
													-StrToMatch $ForegroundWindowTitle
	
	If ($StrFoundInTitle -OR $RgxFoundInTitle) {
		# Bereits das richtige Fenster
		Return
	}

	# Finden wir den Prozess mit einem der gesuchten Titel?
	$ProcInfo = Get-Process-FromTitle -WindowTitles $WindowTitles -TimeoutSec $TimeoutSec
	If ($ProcInfo -ne $Null) {
		Set-Foreground-Window -MainWindowHandle $ProcInfo.MainWindowHandle
	}
}


# Prüft, ob ein String in einem Array gefunden wird, das RegEx Elemente hat
# !Ex
# 	$HasMatch = String-Matches-ItemIn-ArrayWithRegex -ArrayWithRegexes @('Anmelden.*', 'Sign On.*') -StrToMatch 'Anmelden'
Function String-Matches-ItemIn-ArrayWithRegex() {
	[CmdletBinding()]
	Param( 
		[Parameter(Mandatory, Position=0)]
		[String[]]$ArrayWithRegexes,
		[Parameter(Mandatory, Position=1)]
		[AllowEmptyString()]
		[AllowNull()][String]$StrToMatch
	)

	If ($ArrayWithRegexes -eq $Null) { Return $False }
	If ([String]::IsNullOrWhiteSpace($StrToMatch)) { Return $False }

	$Res = @($ArrayWithRegexes | ? { $StrToMatch -match $_ })
	Return $Res.Count -gt 0
}



# Wartet bis ein Fenster mit einem Titel erscheint 
# Liefert 
# - die ProcessID
# - den gefundenen Fenster-Titel
# 
# $ProcessID, $WinTitle = Wait-For-Window @('Cisco AnyConnect | Nosergroup Remote Access', 'Cisco AnyConnect | access.nosergroup.com')
Function Wait-For-Window($WindowTitleMatches, $ExpectedExeFileName = $null) {
	Log 4 "Warte auf einen dieser Fenster-Titel:" -ForegroundColor DarkGray
	$WindowTitleMatches | % { Log 5 "'$_'" -ForegroundColor Magenta }
	Log 4 "Aktiver Fenster-Titel:" -ForegroundColor DarkGray
	$LastWindowTitle = ''
	$Cnt = 0
	While (1) {
		$Cnt++
		# Allenfalls versuchen, die erwartete Applikation in den Vordergrund zu holen,
		# weil Cisco dauernd den Fokus stiehlt :-(
		If ($Cnt % 4 -eq 0) {
			# Versuchen, das Fenster via Exe in den Vordergrund zu holen
			If ($ExpectedExeFileName -ne $null) {
				Wait-For-ExeRunning-And-SetToForeground -ExeFile $ExpectedExeFileName
			}
			# Wenn das Aktivieren via Exe nicht klappte, 
			# versuchen, das Fenster via Titel in den Vordergrund zu holen
			$WindowHandle, $ForegroundWindowTitle = Get-Foreground-WinTitle
			$StrFoundInTitle = $WindowTitleMatches -contains $ForegroundWindowTitle
			$RgxFoundInTitle = String-Matches-ItemIn-ArrayWithRegex -ArrayWithRegexes $WindowTitleMatches `
															-StrToMatch $ForegroundWindowTitle
			
			If ($StrFoundInTitle -eq $False -and $RgxFoundInTitle -eq $False) {
				# Das Fenster ist nicht aktiv, also versuchen, es zu setzen
				Set-Foreground-WinTitle -WindowTitles $WindowTitleMatches -TimeoutSec 3
			}
		}
		
		$WindowHandle, $WinTitle = Get-Foreground-WinTitle
		If ($LastWindowTitle -ne $WinTitle) {
			$LastWindowTitle = $WinTitle
			Log 5 "$($WinTitle)" -ForegroundColor DarkGray
		}
		
		# Haben wir das gesuchte Fenster gefunden?
		$HasMatch = String-Matches-ItemIn-ArrayWithRegex -ArrayWithRegexes $WindowTitleMatches `
						-StrToMatch $WinTitle
		If ($HasMatch) {
			Return @((Get-ProcessID-From_HWND $WindowHandle), $WinTitle)
		}
		Start-Sleep -Milliseconds 250
	}
}


# Default-Delay für SendKeys
Function Sleep-KeyDelay($Delay=250) {
	Start-Sleep -Milliseconds $Delay
}



# Ersetzt die Zeichen in $Data auf sichere Art und Weise, 
# so dass ersetzte Zeichen nicht rekursiv wieder ersetzt werden
# Ex
# $SrcRepl = @(
# 	@('{', '{{}'),
# 	@('}', '{}}'),
# 	@('+', '{+}'),
# 	@('^', '{^}'),
# 	@('%', '{%}'),
# 	@('~', '{~}'),
# 	@('(', '{(}'),
# 	@(')', '{)}'),
# 	@('[', '{[}'),
# 	@(']', '{]}')
# )
# 
# $Data = ' + ^ ~ % ( ) { } [ ] abcd1234'
Function Replace-Chars-safe() {
	[CmdletBinding()]
	Param (
		[Parameter(Mandatory)]
		[String]$Data,
		
		[Parameter(Mandatory)]
		[String[][]]$SrcRepl
	)
	
	If ($Data -eq $null) { Return $Data }
	If ($SrcRepl -eq $null) { Return $Data }
	If ($SrcRepl.Count -eq 0) { Return $Data }
	
	
	$Res = [System.Text.StringBuilder]''
	ForEach ($Chr In $Data.ToCharArray()) {
		$WasReplaced = $False
		# Passt ein Zeichen in $Src?
		For ($i = 0; $i -lt $SrcRepl.Count; $i++) {
			If ($Chr -eq $SrcRepl[$i][0]) {
				$WasReplaced = $true
				$Null = $Res.Append($SrcRepl[$i][1])
				Break
			}
		}
		# Nichts gefunden: Das Originalzeichen übertragen
		If ($WasReplaced -eq $false) {
			$Null = $Res.Append($Chr)
		}
	}
	Return $Res.ToString()
}


# Escape reserved Chars fpr SendKeys
Function Escape-SendKeys($Data) {
	# Escapen der Daten, die ja keine reservierten Begriffe wie {Enter} beinhaltet
	# https://docs.microsoft.com/en-us/dotnet/api/system.windows.forms.sendkeys.send?view=net-5.0
	
	# Some Chars have special meanings to SendKeys. 
	# 	To specify one of these characters, enclose it within braces ({})
	# 	For example, to specify the plus sign, use "{+}". 
	
	$SrcRepl = @(
		#  To specify brace characters, use "{{}" and "{}}". 
		@('{', '{{}'),
		@('}', '{}}'),
		# The plus sign (+), 
		@('+', '{+}'),
		# caret (^), 
		@('^', '{^}'),
		# percent sign (%), 
		@('%', '{%}'),
		# tilde (~), 
		@('~', '{~}'),
		# and parentheses () 
		@('(', '{(}'),
		@(')', '{)}'),
		# Brackets ([ ]) have no special meaning to SendKeys, 
		# but you must enclose them in braces. 
		@('[', '{[}'),
		@(']', '{]}')
	)
	
	Return Replace-Chars-safe $Data $SrcRepl
}


# Schickt Tastatur-Daten und wartet danach kurz
Function SendWait() {
	[CmdletBinding()]
	Param (
		# Data: Diese Daten werden escaped, so dass nicht zufällig benannte Tasten verschickt werden
		[Parameter(Mandatory)]
		[AllowEmptyString()]
		$Data,
		[Parameter(Mandatory)]
		[AllowEmptyString()]
		[AllowNull()][String]$PostData
	)
	
	# Die Daten schicken
	If ([String]::IsNullOrEmpty($Data) -eq $false) {
		[System.Windows.Forms.SendKeys]::SendWait( (Escape-SendKeys $Data) )
		Sleep-KeyDelay
	}
	
	# Die abschliessenden Tastensequenzen schicken
	If ([String]::IsNullOrEmpty($PostData) -eq $false) {
		[System.Windows.Forms.SendKeys]::SendWait($PostData)
		Sleep-KeyDelay
	}
}

#Endregion Win32 Tools


# Get-Phonetic-Password
# Get-Phonetic-Password -PasswordTemplate 'dCvcvdcvcvdCvcvdcvcv'
Function Get-Phonetic-Password($PasswordTemplate = $null) {
	# l = Lowercase Alphabet
	# U = Uppercase Alphabet
	# d = Decimal numbers
	# . = Sonderzeichen
	# v = Lowercase Vowels
	# V = Uppercase Vowels
	# c = Lowercase Consanant
	# C = Uppercase Consanant
	# * = Any defined character in the sets
	
	If ([String]::IsNullOrEmpty($PasswordTemplate)) { $PasswordTemplate = '.Cvcvdcvcv' }
	
	# tomtom
	$LeftHandKeys = '§°1+2"3*4ç5%6qwertasdfg<>yxcvb'
	
	$CharSets = New-Object System.Collections.Hashtable
	# l = Lowercase Alphabet
	$CharSets.Add('l', 'abcdefghkmnpqrstuvwxz'.ToLower())
	# U = Uppercase Alphabet
	$CharSets.Add('U', 'ABCDEFGHKLMNPQRSTUVWXZ'.ToUpper())
	# d = Decimal numbers
	$CharSets.Add('d', '123456789')
	# . = Sonderzeichen
	$CharSets.Add('.', '!$%()-.')
	# v = Lowercase Vowels
	$CharSets.Add('v', 'aeiou')
	# V = Uppercase Vowels
	$CharSets.Add('V', 'AEIOU')
	# c = Lowercase Consanant
	$CharSets.Add('c', 'abcdfghkmnpqrstvwxz'.ToLower())
	# C = Uppercase Consanant
	$CharSets.Add('C', 'ABCDFGHKLMNPQRSTVWXZ'.ToUpper())
	
	# * = Any defined character in the sets
	$AllUniqueChars = (($CharSets.Values -join '') -split '' | select -Unique) -join ''
	$CharSets.Add('*', $AllUniqueChars)
	
	
	Do {
		$NewPassword = ''
		
		# Jede Stelle im Passwort einen Zufallswert generieren
		$RandomData = New-Object 'System.Byte[]' $PasswordTemplate.Length
		$Rnd = New-Object System.Security.Cryptography.RNGCryptoServiceProvider
		$Rnd.GetBytes($RandomData)
		
		# Für jede Stelle im Template das Zeichen generieren 
		For ($i = 0; $i -lt $PasswordTemplate.Length; $i++) {
			$CharSetCharSelector = $PasswordTemplate[$i]
			$Asciiset = $CharSets["$CharSetCharSelector"]
			$NewPassword += $Asciiset[($RandomData[$i] % $Asciiset.Length)]
		}
	}
	# Ein Zeichen darf nicht mehr als 2x vorkommen
	While ($NewPassword -match '(.)(.*\1){2}')

	# Send the password to the Clipboard
	# $NewPassword | clip

	# Return the password in Plaintext and as Secure-String
	# Return $NewPassword, (CONVERTTO-Securestring $NewPassword -asplaintext -force)
	Return $NewPassword
}


# "C:\RemoveFileSecure\test1.txt"  | Remove-FileSecure -DeleteAfterOverwrite #-Confirm:$false #-WhatIf 
# Get-ChildItem c:\RemoveFileSecure -Filter *.txt  | Remove-FileSecure -DeleteAfterOverwrite #-Confirm:$false #-WhatIf 
# 200805
Function Remove-FileSecure {
    [CmdletBinding(SupportsShouldProcess, ConfirmImpact="High")]
    Param( 
        [Parameter(Mandatory, ValueFromPipeline)]
        $File,
        [Parameter()]
        [Switch]$DeleteAfterOverwrite = $false
    )

    begin {
        $rnd = new-object System.Security.Cryptography.RNGCryptoServiceProvider
    }

    process {
        $RetObj = $null
            
        if ((Test-Path $File -PathType Leaf) -and $pscmdlet.ShouldProcess($File)) {
            $oFile = $File
            if( !($oFile -is [System.IO.FileInfo]) ) {
                $oFile = new-object System.IO.FileInfo($File)
            }

            $FileLen = $oFile.length
            # Log 4 $oFile.FullName
            $Stream = $oFile.OpenWrite()
            try {
                $StopWatch = new-object system.diagnostics.stopwatch
                $StopWatch.Start()

                Write-Progress -Activity $oFile.FullName -Status "Write" -PercentComplete 0 -CurrentOperation ""

                [long]$i = 0
                $Buffer = new-object byte[](1024*1024)
                while( $i -lt $FileLen ) {
                    $rnd.GetBytes($Buffer)
                    $rest = $FileLen - $i
                    if( $rest -gt (1024*1024) ) {
                        $Stream.Write($Buffer, 0, $Buffer.length)
                        $i += $Buffer.LongLength
                    } else {
                        $Stream.Write($Buffer, 0, $rest)
                        $i += $rest
                    }
                    [double]$p = [double]$i / [double]$FileLen
                    [long]$remaining = [double]$StopWatch.ElapsedMilliseconds / $p - [double]$StopWatch.ElapsedMilliseconds
                    Write-Progress -Activity $oFile.FullName -Status "Write" -PercentComplete ($p * 100) -CurrentOperation "" -SecondsRemaining ($remaining/1000)
                }
                $StopWatch.Stop()
            
			} finally {
                $Stream.Close()
                if( $DeleteAfterOverwrite ) {
                    $j  = Remove-Item $oFile.FullName -Force -Confirm:$false
					Start-Sleep -Milliseconds 250
                    $RetObj = new-object PSObject -Property @{File = $oFile; Wiped=$true; Deleted=(-Not (Test-Path $oFile)) }
                } else {
                    $RetObj = new-object PSObject -Property @{File = $oFile; Wiped=$true; Deleted=$false}
                }
            }
        } else {
            $RetObj = new-object PSObject -Property @{File = $File; Wiped=$false; Deleted=$false}
        }
        return $RetObj
    }
}


Function If-File-Exists($FileName) {
	If ([String]::IsNullOrEmpty($FileName)) { Return $False }
	If (Test-Path -LiteralPath $FileName -PathType Leaf) { Return $True }
	Return $False
}


# Sucht rekursiv das neuste File gemäss Muster
# e.g Filter:
# 'naxxxx@user.nosergroup.lan*.*'
# Liefert eine FileInfo
Function Find-Newest-File() {
	[CmdletBinding()]
	Param( 
		[Parameter(Mandatory, ValueFromPipeline)]
		[String]$RootPath,
		[Parameter(Mandatory, ValueFromPipeline)]
		[String]$Filter,
		[Parameter(Mandatory, ValueFromPipeline)]
		[DateTime]$NotOlderThan
	)

	# Alle Files, die neuer als $NotOlderThan sind
	@(Get-ChildItem $RootPath -File -Filter $Filter -Recurse -ErrorAction SilentlyContinue | `
							? { $_.LastWriteTime -ge $NotOlderThan } | `
							Sort LastWriteTime -Descending | select -First 1)
}


Function Set-RunOnce() {
  <#
      .SYNOPSIS
      Sets a Runonce-Key in the Computer-Registry. Every Program which will be added will run once at system startup.
      This Command can be used to configure a computer at startup.
		
		Besser mit KHLM und nicht mit HKCU\RunOnce arbeiten,
 		weil wenn der Script-Kontext temporär z.B. zu nypadmin geändert wird,
 		dann klappt HKCU nicht, weil sich beim nächsten Login ja der MA anmeldet
 
      .EXAMPLE
      Set-Runonce -command '%systemroot%\System32\WindowsPowerShell\v1.0\powershell.exe -executionpolicy bypass -file c:\Scripts\start.ps1'

      Sets a Key to run Powershell at startup and execute C:\Scripts\start.ps1
 
      .NOTES
      Author: Holger Voges
      Version: 1.0
      Date: 2018-08-17
 
      .LINK
      https://www.netz-weise-it.training/
  #>
    [CmdletBinding()]
    Param (
		#The Name of the Registry Key in the Autorun-Key.
		[String]$KeyName = 'Run',
		#Command to run
		[String]$Command,
		[Switch]$HKLM,
		[Switch]$HKCU
    ) 

	# Default: HKLM
	If ($HKLM -eq $False -and $HKCU -eq $False) { $HKLM = $True }
	
	$RegkeyPaths = @()
	If ($HKLM) { $RegkeyPaths += 'HKLM:\Software\Microsoft\Windows\CurrentVersion\RunOnce' }
	If ($HKCU) { $RegkeyPaths += 'HKCU:\Software\Microsoft\Windows\CurrentVersion\RunOnce' }

	$RegkeyPaths | % {
		$RegkeyPath = $_
		If (-not ((Get-Item -Path $RegkeyPath).$KeyName )) {
			$Res = New-ItemProperty -Path $RegkeyPath -Name $KeyName -Value $Command -PropertyType ExpandString -Force
		} Else {
			$Res = Set-ItemProperty -Path $RegkeyPath -Name $KeyName -Value $Command -PropertyType ExpandString -Force
		}
	}
}

Function Restart($TimeoutSec = 1) {
	$Cnt = 0
	While ($Cnt -lt $TimeoutSec) {
		Write-Host ('.' * ($TimeoutSec-$Cnt) + ' ' * $TimeoutSec + "`r") -NoNewLine
		Start-Sleep -Seconds 1
		$Cnt++
	}
	Write-Host (" `r`n")
	Restart-Computer
}



# Lösst den User die Anyconnect-CoreVpnPredeploy Setup datei wählen,
# wenn es mehr als eine gibt
# Ex
# 	Select-Anyconnect-CoreVpnPredeploy -Folder $pfad -PreferedVersion '4.10.02086'
# Return
# 	Den gewählten Dateiname (Fullname)
#           > Wird wiederholt, bis der User eine Datei wählt
# 	Null 	> Keine passende Datei gefunden
Function Select-Anyconnect-CoreVpnPredeploy() {
	[CmdletBinding()]
	Param (
		# Ordner, in dem wir suchen
		[String]$Folder,
		# E.g. 4.10.02086
		[String]$PreferedVersion = $null
	) 
	
	$FileFilter = 'anyconnect-win-*-core-vpn-predeploy-k9.msi'
	
	$AllCoreVpnPredeployFiles = @(Get-ChildItem -LiteralPath $Folder -Filter $FileFilter | sort Name)
	
	Switch ($AllCoreVpnPredeployFiles.Length) {
		0 {
			# Keine Datei gefunden
			return $null
		}
		1 {
			# Eine Datei gefunden
			return $AllCoreVpnPredeployFiles | select -first 1 -ExpandProperty FullName
		}
		Default {
			If ([String]::IsNullOrEmpty($PreferedVersion)) {
				$MatchVpnPredeployFile = $null
				$OtherVpnPredeployFile = $AllCoreVpnPredeployFiles
			} Else {
				$MatchVpnPredeployFile = $AllCoreVpnPredeployFiles | ? Name -Like "*$($PreferedVersion)*"
				$OtherVpnPredeployFile = $AllCoreVpnPredeployFiles | ? Name -NotLike "*$($PreferedVersion)*"
			}

			$AuswahlListe = @()
			$AuswahlListe += New-Object PSObject -Property @{ FileName = "Bitte die AnyConnect Core VPN Predeploy Datei wählen" + ' ' * 20; FullName=' ' * 120 }
			$AuswahlListe += New-Object PSObject -Property @{ FileName = ""; FullName=$null }
			
			If ($MatchVpnPredeployFile -ne $null) {
				$AuswahlListe += New-Object PSObject -Property @{ FileName = "Die aktuelle Version ist:"; FullName=$null }
				$AuswahlListe += New-Object PSObject -Property @{ FileName = ($MatchVpnPredeployFile | Select -ExpandProperty Name); `
																				  FullName=($MatchVpnPredeployFile | Select -ExpandProperty FullName)  }
				$AuswahlListe += New-Object PSObject -Property @{ FileName = ''; FullName=$null  }

				$AuswahlListe += New-Object PSObject -Property @{ FileName = 'Für Beta-Tester:'; FullName=$null  }
			}
			
			$OtherVpnPredeployFile | % {
				$AuswahlListe += New-Object PSObject -Property @{ FileName = $_.Name; FullName=$_.FullName }
			}
			
			# Der User soll wählen
			$UserSelection = ""
			while ([String]::IsNullOrEmpty($UserSelection)) {
				$UserSelection = ( ($AuswahlListe) | Select FileName, FullName | Out-GridView -PassThru) | select -ExpandProperty FullName
				# Wahl validieren: Der User muss eine Datei der Liste wählen und nicht einen Hilfstext
				$UserSelection = ($AllCoreVpnPredeployFiles | ? FullName -eq $UserSelection).FullName
			}
			Return $UserSelection
		}
	}
}


#Region Zertifikat-Management

# Liefert rekursiv alle Zertifikate
# Selber implementiert, weil 
# 		$Res = Get-ChildItem Cert:\CurrentUser\ -Recurse
# Manchmal diesen Fehler erzeugt:
# 		Get-ChildItem : The specified network resource or device is no longer available
# 220223
Function Get-Certs($Path = 'Cert:\', [Switch]$SilentSkipErrors = $True) {
	Try {
		$Items = Get-ChildItem $Path -ErrorAction Stop
	} Catch {
		If ($SilentSkipErrors -eq $false) {
			Write-Host "Fehler beim Lesen von: $Path - ignoriert" -ForegroundColor Red
		}
		Return
	}
	ForEach ($Item In $Items) {
		Switch ($Item.GetType().Name) {
			'X509StoreLocation' {
				# Ist:
				# - Cert:\CurrentUser\
				# - Cert:\LocalMachine\
				# Write-Host "X509StoreLocation"
				Get-Certs ("{0}:\{1}" -f $Item.PSDrive, $Item.Location)
			}
			'X509Store' {
				# Write-Host "X509Store"
				Get-Certs ("{0}:\{1}\{2}" -f $Item.PSDrive, $Item.Location, $Item.Name)
			}
			'X509Certificate2' {
				# Write-Host "X509Certificate2"
				# Das Zertifikat zurückgeben
				$Item
			}
			Default {
				Write-Host "Nicht behandelter Typ: $($Item.GetType().Name)" -ForegroundColor Red
			}
		}
	}
}


# Sucht die NPID-User-Zertifikate
Function Get-UserNosergroupLan-Certificates() {
	Get-Certs | ? Subject -Like '*CN=n*@user.nosergroup.lan*'
}

# Sucht aufgrund der Zertifikate die NPID
Function Get-User-NPID() {
	$rgx = 'CN=(?<NPIDFull>(?<NPID>n[a-zA-Z]\d{4})@user.nosergroup.lan)'
	$Res = @()
	Get-UserNosergroupLan-Certificates | % {
		If ($_.Subject -match $rgx) {
			$Res += $matches.npid
		}
	}
	$Res
}

# Löscht alle NPID-User-Zertifikate
Function Remove-UserNosergroupLan-Certificates() {
	Get-Certs | ? Subject -Like '*CN=n*@user.nosergroup.lan*' | Remove-Item
}

# Löscht alle abgelaufenen NPID-User-Zertifikate
Function Remove-Obsolete-UserNosergroupLan-Certificates() {
	# Die NPID-Zertifikate auslesen und anzeigen
	$AlleNPIDZertifikate = @(Get-Certs | ? Subject -Like '*CN=n*@user.nosergroup.lan*' | `
		select @{ N = 'Zertifikat'; Ex = { $_.DnsNameList } }, `
		@{ N = 'Gueltig bis'; Ex = { $_.NotAfter.ToString('dd.MM.yyyy') } }, `
		@{ N = 'Tage'; Ex = { [Math]::Truncate(($_.NotAfter - (Get-Date)).TotalDays) } })
		
	# Alle alten Zertifikate löschen
	$AlleNPIDZertifikate | ? Tage -le 0 | Remove-Item
}

#Endregion Zertifikat-Management


#Region Cisco AnyConnect Funcs


# Cisco AnyConnect Secure Mobility Client muss nach der Deinstallation doppelt nachgereinigt werden
# Diese Funktion
# - Bricht ab, wenn Cisco AnyConnect Secure Mobility Client nicht installiert ist
# - Sucht den exakten Applikationsnamen und installationspfad
# - Startet die Standard-Deinstallation
# - Startet den RevoUninstaller im Advanced Mode
Function Uninstall-Cisco_AnyConnect_Secure_Mobility_Client() {
	
	$AppName = 'Cisco AnyConnect Secure Mobility Client'
	
	# Haben wir die Applikation installiert?
	$FoundAppName, $FoundAppInstallDir = Get-RevoUninstaller-ApplicatonDir $AppName
	# Die Applikation ist 
	If ([String]::IsNullOrEmpty($FoundAppName) -or [String]::IsNullOrEmpty($FoundAppInstallDir)) {
		# Nichts zu tun
		Return
	}
	
	## Die Standard-Deinstallation starten
	
	
	
	## Die RevoUninstaller Berienigung starten
	# Kurz warten
	Start-Sleep -Seconds 6
	Start-RevoUninstaller-Advanced-Wipe $FoundAppName, $FoundAppInstallDir
}




# Wartet auf die Cisco-Links auf dem Desktop
# und löscht sie
# - Cisco AMP for Endpoints Connector.lnk
Function Remove-Cisco-Desktop-Lnk-Files() {
	Log 2 'Warte auf die Cisco-Links auf dem Desktop'
	Start-Sleep -Milliseconds 20000
	Do {
		Log 2 'Warte auf die Cisco-Links auf dem Desktop'
		Start-Sleep -Milliseconds 5000
		$Dirs = @('c:\ProgramData\', 'c:\Users\')
		$Res = $Dirs | % { Get-ChildItem -Force -ErrorAction Ignore -Recurse -LiteralPath $_ -Filter '*.lnk' | ? FullName -Like '*\Desktop\*Cisco*.lnk' }
		# Wiederholen, so lange wir keine Lnk-Files finden
	} While ($Res -eq $null)

	Log 1 'Cisco-Links gefunden - Löschen'
	$SB = { 
		$Dirs = @('c:\ProgramData\', 'c:\Users\')
		$Dirs | % {
			Get-ChildItem -Force -ErrorAction Ignore -Recurse -LiteralPath $_ -Filter '*.lnk' | ? FullName -Like '*\Desktop\*Cisco*.lnk' | Remove-Item -Force 
		}
	}
	Invoke-Elevated-PSScript -ScriptBlock $SB
}


# Löscht die Cisco AnyConnect MRU-Liste
Function Remove-Cisco-MRU-Profile() {
	$ProfileDir = 'c:\ProgramData\Cisco\Cisco AnyConnect Secure Mobility Client\Profile\'
	# c:\Users\schittli\AppData\Local\Cisco\Cisco AnyConnect Secure Mobility Client\preferences.xml
	$PreferencesFile = "$($env:LOCALAPPDATA)\Cisco\Cisco AnyConnect Secure Mobility Client\preferences.xml"
	
	# Das Preferences-File löschen, weil sonst der Login-Dialog bereits einen Usernamen hat
	Remove-Item -Force -LiteralPath $PreferencesFile -ErrorAction SilentlyContinue
	
	# Im Profil-File die MRU-Liste löschen, wenn es ein Noser-Profil ist
	$ProfileFiles = @(Get-Childitem -LiteralPath $ProfileDir -Recurse -include '*.xml' | ? {$_ | Select-String 'noser' } | % { $_.FullName })
	
	If ($ProfileFiles.Count -gt 0) {
		Foreach ($ProfileFile in $ProfileFiles) {
			[XML]$xml = Get-Content $ProfileFile
			If ($xml.AnyConnectProfile.ServerList.GetType().Name -ne 'String') {
				$xml.AnyConnectProfile.ServerList.RemoveAll()
				$xml.save($ProfileFile)
			}
		}
	}
}


# Liefert True, wenn vpncli.exe gefunden wird
Function Is-Cisco-AnyConnect-Installed() {
	$VpnCliExe = Get-ChildItem ${env:ProgramFiles(x86)} -Recurse -Filter 'vpncli.exe' -ErrorAction SilentlyContinue | Select -First 1 -Expandproperty Fullname
	If ([String]::IsNullOrEmpty($VpnCliExe)) {
		Return $False
	}
	Return $True
}

# Testet die VPN-Verbindung
# True	Verbunden
# False	Nicht Verbunden
# Null	Unbekannt
Function Is-Cisco-Vpn-Connected() {
	$VpnCliExe = Get-ChildItem ${env:ProgramFiles(x86)} -Recurse -Filter 'vpncli.exe' -ErrorAction SilentlyContinue | Select -First 1 -Expandproperty Fullname
	If ([String]::IsNullOrEmpty($VpnCliExe)) {
		Log 4 'Cisco AnyConnect ist nicht installiert - vpncli.exe fehlt' -ForegroundColor Red
		Return
	}
	
	$State = . $VpnCliExe state
	# Ein vorheriges Resultat löschen
	Try { $Matches.State = $null }
	Catch {}
	If (($State -join "`n") -match '(?in)state:\s(?<State>.*)') {
		Return $Matches.State -eq 'Connected'
	} Else {
		Return $null
	}
}


# Trennt die VPN-Verbindung
Function Disconnect-Cisco-Vpn() {
	$VpnCliExe = Get-ChildItem ${env:ProgramFiles(x86)} -Recurse -Filter 'vpncli.exe' -ErrorAction SilentlyContinue | Select -First 1 -Expandproperty Fullname
	$State = . $VpnCliExe disconnect
	Start-Sleep -Milliseconds 3000
}


# Startet das Cisco AnyConnect UI im Vordergrund
Function Start-Cisco-AnyConnect-UI() {
	$VpnUiExe = Get-ChildItem ${env:ProgramFiles(x86)} -Recurse -Filter 'vpnui.exe' -ErrorAction SilentlyContinue | Select -First 1 -Expandproperty Fullname
	$ProcessInfo = Start-Exe-Foreground -ExeFile $VpnUiExe -ShowInfo:$False
	Start-Sleep -Milliseconds 750
	# Das Fenster in den Vordergrund holen
	$ProcVpnUI = Get-Process -Name 'vpnui'
	If ($ProcVpnUI -ne $Null) {
		Set-Foreground-Window $ProcVpnUI.MainWindowHandle		
	}
}


Function Kill-Cisco-VPN-Processes() {
	# Terminate all vpnui processes.
	Get-Process | ForEach-Object {if($_.ProcessName.ToLower() -eq ([IO.Path]::GetFileNameWithoutExtension($CiscoVpnUiExe)))
	{ $Id = $_.Id; Stop-Process $Id -Force; Log 2 "Process vpnui with id: $Id was stopped"} }

	# Terminate all vpncli processes.
	Get-Process | ForEach-Object {if($_.ProcessName.ToLower() -eq ([IO.Path]::GetFileNameWithoutExtension($CiscoVpnCliExe)))
	{$ Id = $_.Id; Stop-Process $Id -Force; Log 2 "Process vpncli with id: $Id was stopped"} }
	
	Start-Sleep -Milliseconds 2500
}


# Liefert die Cisco-SW, die als Vorereitung fürs Onboarding deinstaliert wird
# Arbeitet mit $UninstallCiscoSW_Whitelist
# 
# Get-CiscoSW-ToUninstall -Property DisplayVersion, VersionMajor, Installdate, Uninstallstring
Function Get-CiscoSW-ToUninstall([String[]]$Property, [String[]]$IncludeProgram = '*Cisco *') {
	$Splat = @{ }
	If ($Property) { $Splat += @{ Property = $Property } }
	If ($IncludeProgram) { $Splat += @{ IncludeProgram = $IncludeProgram } }
	
	Get-Installed-Software @Splat | ? { If ($UninstallCiscoSW_Whitelist -match $_.ProgramName) { $false } Else { $true } }
}



#Endregion Cisco AnyConnect Funcs


#Region Nosergroup.lan spezifische Funcs


# Nützt wget mit einem Timeout und gibt optional einen Fehler aus
# Returns:
#  Fehler: 	$Null
#  Efolg: 	$Content
Function Get-Wget($Url, $TimeoutSec = $Null, [Switch]$ShowError = $False) {
	$GotError = $False
	Try {
		$Splat = @{ }
		If ($TimeoutSec -ne $Null) { $Splat += @{ TimeoutSec = $TimeoutSec } }
		$Content = wget $Url -UseBasicParsing @Splat
		Return $Content
	} Catch {
		$GotError = $True
		$MessageId = ('{0:x}' -f $_.Exception.HResult).Trim([char]0)
		$ErrorMessage = ($_.Exception.Message).Trim([char]0) # The network path was not found.	
	}
	
	If ($ShowError -and $GotError) {
		# Write-Host "Fehler: $ErrorMessage"
		log 4 "Fehler: $ErrorMessage"
	}
}


# Haben wir die Webseite, auf der gewarnt wird, dass das Gerät bereits onboarded wurde?
Function Is-Device-AlreadyOnboarded-Warning() {
	# Beim nginx-Fehler, wäre diese URL die richtige gewesen:
	# $URL = 'http://welcome.nosergroup.lan/en-US/index.html'
	
	# Sucht diese Texte:
	# 'to have it removed'
	# 'um es entfernen zu lassen'
	$FehlerText = 'have\s+it\s+removed|es\s+entfernen\s+zu\s+lassen'
	
	# Wenn wir die Seite holen können
	If ($Content = Get-Wget $WelcomeURL -TimeoutSec $WgetTimeoutSec) {
		If ($Content.Content -match $FehlerText) {
			Return $True
		}
	}
	Return $False
}


# Testet, ob die VPN-Verbindung mit dem Zertifikat hergestellt wurde
Function Is-Cisco-Vpn-Connected-ByCertificate() {
	If ((Is-Cisco-Vpn-Connected) -ne $True) { Return $False }
	
	# Diese Webseite ist nur per Zertifikat erreichbar,
	# sonst antwortet der Proxy
	If ($Content = Get-Wget $WelcomeURL -TimeoutSec $WgetTimeoutSec) {
		If ($Content.Headers.Server -like 'nginx*') {
			Return $False
		} Else {
			Return $True
		}
	}
}



# Killt den Cisco AnyConnect Client
# Startet das Cisco AnyConnect UI
# Initiiert die VPN-Verbindung mit Username und Password
Function Cisco-VPN-Connect($Server, $NpidFqdn, $SecurePassword) {

	If (Is-Cisco-Vpn-Connected) {
		Log 1 'Die VPN-Verbindung ist bereits aufgebaut'
		Return
	}
	
	Log 1 'Prüfe Elevation'
	If (IsNot-Script-Elevated) {
		Log 4 'Das Script muss im elevated Kontext laufen!' -ForegroundColor Red
		Log 4 'Abbruch.' -ForegroundColor Red
		Return
	}
	
	Log 1 'Prüfe RDP'
	If (Is-RDP-Session-Active) {
		Log 1 'In der RDP-Session kann das VPN nicht gestartet werden' -ForegroundColor Red
		# Beenden und sicherstellen, dass der User die Meldung sieht
		Stop-Script-Wait $WaitOnEnd
	}

	
	Log 1 'Beende die Cisco-Prozesse'
	Kill-Cisco-VPN-Processes

	Log 1 'Lösche die MRU-Liste'
	Remove-Cisco-MRU-Profile

	Log 1 'Starte Cisco AnyConnect'
	$ProcessInfo = Start-Exe-Foreground -ExeFile $CiscoVpnUiExe
	
	Start-Sleep -Milliseconds 3000
	
	Log 1 'Initiiere die Verbindung'
	SendWait '' '{Tab}'
	SendWait '' '{Tab}'
	SendWait '' '{Tab}'
	SendWait "$Server" '{Enter}'

	Log 1 'Warte auf den Passwort-Dialog'
	$ProcessID, $WinTitle = Wait-For-Window @('Cisco AnyConnect | Nosergroup Remote Access', 'Cisco AnyConnect | access.nosergroup.com')
	Start-Sleep -Milliseconds 1500

	Log 1 'Starte die die Anmeldung'
	# Den obersten Eintrag der Gruppe auswählen "Nosergroup VPN Access", d.h. ohne Zertifikat
	# SendWait "+{Tab}"
	# SendWait "%{Down}"
	# SendWait "{Home}"
	# SendWait "{Enter}"
	# Username wählen
	# SendWait "{Tab}"
	# Username eingeben
	SendWait "$NpidFqdn" '{Tab}'
	# Passwort eingeben
	# $Password = [System.Net.NetworkCredential]::new('', $SecurePassword).Password
	# SendWait "$Password{Enter}"
	# Log 4 "$([System.Net.NetworkCredential]::new('', $SecurePassword).Password)"
	SendWait "$([System.Net.NetworkCredential]::new('', $SecurePassword).Password)" '{Enter}'
	
	Do { Start-Sleep -Milliseconds 500
	} While ( -not (Is-Cisco-Vpn-Connected))
	Start-Sleep -Milliseconds 1500
}



# Bug Cisco: Manchmal bricht Cisco die erste VPN-Verbindung sofort wieder ab
# Stellt die Cisco-VPN-Vebrindung her und stellt sicher, dass sie nach $TimeoutSec immer noch aktiv ist
Function Cisco-VPN-Connect-AndWait($Server, $NpidFqdn, $SecurePassword, [Int]$TimeoutSec = 15) {
	Log 0 'Starte die VPN-Verbindung'
	Log 1 'Deaktiviere die VPN-Zertifikat-Wahl'
	. $SetCiscoAnyConnectDefaultProfile_ps1 -DisableAutomaticCertSelection
	Log 1 'Starte VPN mit Username / PW'
	Cisco-VPN-Connect $Server $NpidFqdn $SecurePassword

	# Wenn wir nicht warten müssen, sind wir fertig
	If ($TimeoutSec -eq $Null -OR $TimeoutSec -le 0) {
		Return
	}

	Log 1 "Bitte Warten!: prüfe, ob das VPN $($TimeoutSec) Sekunden stabil bleibt" -ForegroundColor Yellow -BackgroundColor DarkRed
	$SleepCntSeconds = 0
	Do {
		Log 2 ('.' * ($TimeoutSec - $SleepCntSeconds)) -NoNewline -Append -ReplaceLine -ClrToEol 
		Start-Sleep -Milliseconds 1000
		# Sollte die VPN-Verbindung vor userer Wartezeit abbrechen, starten wir sie sofort wieder
		If ((Is-Cisco-Vpn-Connected) -ne $True) {
			Log 2 ''
			Log 2 '… VPN-Verbindung abgebrochen!' -ClrToEol -ForegroundColor Red
			Log 2 'Neustart: Starte VPN nochmals mit Username / PW'
			Start-Sleep -Milliseconds 1500
			Cisco-VPN-Connect $Server $NpidFqdn $SecurePassword
			$SleepCntSeconds = 0
		}
	}  While ($SleepCntSeconds++ -lt $TimeoutSec)
}

#Endregion Nosergroup.lan spezifische Funcs


#Region Window Management

# Liefert vom Primary Screen das WorkingArea
Function Get-Screen-Primary-WorkingArea() {
	# [System.Windows.Forms.Screen]::AllScreens
	
	# Location : {X=0,Y=0}
	# Size     : {Width=2048, Height=1112}
	# X        : 0
	# Y        : 0
	# Width    : 2048
	# Height   : 1112
	# Left     : 0
	# Top      : 0
	# Right    : 2048
	# Bottom   : 1112
	# IsEmpty  : False
	([System.Windows.Forms.Screen]::AllScreens | ? Primary).WorkingArea
}


# Liefert für einen Prozess das MainWindowHandle
Function Get-Process-MainWindowHandle() {
    [CmdletBinding()]
    Param (
		[System.Nullable[Int]]$ProcessID = $Null,
		[Parameter(ValueFromPipelineByPropertyName=$True)]
      [String]$ProcessName = $Null
    )

	If ([String]::IsNullOrEmpty($ProcessName) -and $ProcessID -eq $Null) {
		Log 4 'Get-Process-MainWindowHandle(): Einer dieser Parameter muss definiert sein!:' -ForegroundColor Red
		Log 5 '-ProcessName'
		Log 5 '-ProcessID'
		Log 4 'Abbruch.'
		Return
	}
	
	$GetProcessSplat = @{}
	If (Has-Value $ProcessName) { $GetProcessSplat += @{ Name = $ProcessName} }
	If ($ProcessID -ne $Null) { $GetProcessSplat += @{ ID = $ProcessID} }

	Return (Get-Process @GetProcessSplat).MainWindowHandle
}

# Get-Process-Window 'PowerShell'
Function Get-Process-Window {
    <#
        .SYNOPSIS
            Retrieve the window size (height,width) and coordinates (x,y) of
            a process window.

        .PARAMETER ProcessName
            Name of the process to determine the window characteristics

        .OUTPUT
            System.Automation.WindowInfo

        .EXAMPLE
            Get-Process-Window powershell | Get-Window
    #>
    [OutputType('System.Automation.WindowInfo')]
    [CmdletBinding()]
    Param (
        [Parameter(ValueFromPipelineByPropertyName=$True)]
        $ProcessName = $Null,
		[System.Nullable[Int]]$ProcessID = $Null,
		[System.Nullable[Int]]$MainWindowHandle = $Null
    )
    Begin { 
		$GetProcessSplat = @{}
		If (Has-Value $ProcessName) { $GetProcessSplat += @{ Name = $ProcessName} }
		If ($ProcessID -ne $Null) { $GetProcessSplat += @{ ID = $ProcessID} }
	}
    Process {
		Function Get-Window($MainWindowHandle) {
            $Rectangle = New-Object RECT
            $Return = [ApiFuncs]::GetWindowRect($MainWindowHandle, [ref]$Rectangle)
            If ($Return) {
                $Height = $Rectangle.Bottom - $Rectangle.Top
                $Width = $Rectangle.Right - $Rectangle.Left
                $Size = New-Object System.Management.Automation.Host.Size -ArgumentList $Width, $Height
                $TopLeft = New-Object System.Management.Automation.Host.Coordinates -ArgumentList $Rectangle.Left, $Rectangle.Top
                $BottomRight = New-Object System.Management.Automation.Host.Coordinates -ArgumentList $Rectangle.Right, $Rectangle.Bottom
                # If ($Rectangle.Top -lt 0 -AND $Rectangle.LEft -lt 0) {
                #     Write-Warning "Window is minimized! Coordinates will not be accurate."
                # }
                $Object = [PSCustomObject]@{
                    ProcessName = $ProcessName
					Width = $Width
					Height = $Height
					Left = $Rectangle.Left
					Top = $Rectangle.Top
					Right = $Rectangle.Right
					Bottom = $Rectangle.Bottom
                }
                $Object.PSTypeNames.insert(0,'System.Automation.WindowInfo')
                $Object
            }
		}
	
		If ($MainWindowHandle) {
			Get-Window $MainWindowHandle
		} Else {
			Get-Process @GetProcessSplat | ForEach {
				Get-Window ((Get-Process @GetProcessSplat).MainWindowHandle)
			}
		}
    }
}


# Set-Process-Window PowerShell -X 2040 -Y 142 -Passthru
# Get-Process powershell | Set-Process-Window  -X 2040 -Y 142 -Passthru
Function Set-Process-Window {
    <#
        .SYNOPSIS
            Sets the window size (height,width) and coordinates (x,y) of
            a process window.

        .PARAMETER ProcessName
            Name of the process to determine the window characteristics

        .PARAMETER X
            Set the position of the window in pixels from the top.

        .PARAMETER Y
            Set the position of the window in pixels from the left.

        .PARAMETER Width
            Set the width of the window.

        .PARAMETER Height
            Set the height of the window.

        .PARAMETER Passthru
            Display the output object of the window.
        .EXAMPLE
            Get-Process powershell | Set-Window -X 2040 -Y 142 -Passthru

            ProcessName Size     TopLeft  BottomRight
            ----------- ----     -------  -----------
            powershell  1262,642 2040,142 3302,784   
    #>
    [OutputType('System.Automation.WindowInfo')]
    [CmdletBinding()]
    Param (
        [parameter(ValueFromPipelineByPropertyName=$True)]
        $ProcessName = $Null,
		[System.Nullable[Int]]$ProcessID = $Null,
		[System.Nullable[Int]]$MainWindowHandle = $Null,
        [Int]$X,
        [Int]$Y,
        [Int]$Width,
        [Int]$Height,
        [switch]$Passthru
    )
    Begin { 
		$GetProcessSplat = @{}
		If (Has-Value $ProcessName) { $GetProcessSplat += @{ Name = $ProcessName} }
		If ($ProcessID -ne $Null) { $GetProcessSplat += @{ ID = $ProcessID} }
	}
    Process {
        $Rectangle = New-Object RECT
		If ($MainWindowHandle -eq $null) {
			$Handle = (Get-Process @GetProcessSplat).MainWindowHandle
		} Else {
			$Handle = $MainWindowHandle
		}

		# Sicherheitshalber:
		Set-WindowStyle $Handle RESTORE
		
        $Return = [ApiFuncs]::GetWindowRect($Handle,[ref]$Rectangle)
        If (-NOT $PSBoundParameters.ContainsKey('Width')) {            
            $Width = $Rectangle.Right - $Rectangle.Left            
        }
        If (-NOT $PSBoundParameters.ContainsKey('Height')) {
            $Height = $Rectangle.Bottom - $Rectangle.Top
        }
        If ($Return) {
            $Return = [ApiFuncs]::MoveWindow($Handle, $x, $y, $Width, $Height,$True)
        }
        If ($PSBoundParameters.ContainsKey('Passthru')) {
            $Rectangle = New-Object RECT
            $Return = [ApiFuncs]::GetWindowRect($Handle,[ref]$Rectangle)
            If ($Return) {
                $Height = $Rectangle.Bottom - $Rectangle.Top
                $Width = $Rectangle.Right - $Rectangle.Left
                $Size = New-Object System.Management.Automation.Host.Size -ArgumentList $Width, $Height
                $TopLeft = New-Object System.Management.Automation.Host.Coordinates -ArgumentList $Rectangle.Left, $Rectangle.Top
                $BottomRight = New-Object System.Management.Automation.Host.Coordinates -ArgumentList $Rectangle.Right, $Rectangle.Bottom
                # If ($Rectangle.Top -lt 0 -AND $Rectangle.LEft -lt 0) {
                #     Write-Warning "Window is minimized! Coordinates will not be accurate."
                # }
                $Object = [PSCustomObject]@{
                    ProcessName = $ProcessName
                    Size = $Size
                    TopLeft = $TopLeft
                    BottomRight = $BottomRight
                }
                $Object.PSTypeNames.Insert(0,'System.Automation.WindowInfo')
                $Object            
            }
        }
    }
}


# Set-WindowStyle MAXIMIZE $MainWindowHandle
Function Set-WindowStyle {
	Param(
		[Parameter()]
		$MainWindowHandle,
		[Parameter()]
		[ValidateSet('FORCEMINIMIZE', 'HIDE', 'MAXIMIZE', 'MINIMIZE', 'RESTORE', 
		'SHOW', 'SHOWDEFAULT', 'SHOWMAXIMIZED', 'SHOWMINIMIZED', 
		'SHOWMINNOACTIVE', 'SHOWNA', 'SHOWNOACTIVATE', 'SHOWNORMAL')]
		$Style = 'SHOW'
	)
	
	$WindowStates = @{
		FORCEMINIMIZE   = 11; HIDE            = 0
		MAXIMIZE        = 3;  MINIMIZE        = 6
		RESTORE         = 9;  SHOW            = 5
		SHOWDEFAULT     = 10; SHOWMAXIMIZED   = 3
		SHOWMINIMIZED   = 2;  SHOWMINNOACTIVE = 7
		SHOWNA          = 8;  SHOWNOACTIVATE  = 4
		SHOWNORMAL      = 1
	}
	Write-Verbose ("Set Window Style {1} on handle {0}" -f $MainWindowHandle, $($WindowStates[$style]))
	
	[ApiFuncs]::ShowWindow($MainWindowHandle, $WindowStates[$Style]) | Out-Null
	Start-Sleep -Milliseconds 250
}


# Berechnet das Delta der Applikations-Fenster
Function Calc-Process-Window-Deltas() {
	# Mit dem aktuellen PowerShell-Prozess arbeiten
	$WindowHandle = (Get-Process -Id $PID).MainWindowHandle

	# Die Bildschirmgrösse
	$ScreenSize = Get-Screen-Primary-WorkingArea
	# PowerShell: die aktuelle Grösse holen
	$PowerShellSizeOri = Get-Process-Window -ProcessID $PID
	# PowerShell: maximieren
	Set-WindowStyle $WindowHandle RESTORE
	Set-WindowStyle $WindowHandle MAXIMIZE
	# PowerShell: die maximierte Grösse holen
	$PowerShellSizeMax = Get-Process-Window -ProcessID $PID
	# PowerShell: Grösse wieder herstellen
	Set-Process-Window -ProcessID $PID -X $PowerShellSizeOri.Left -Y $PowerShellSizeOri.Top -Width $PowerShellSizeOri.Width -Height $PowerShellSizeOri.Height
	
	# Die Delta berechnen
	$DeltaX = [Math]::Abs(0 - $PowerShellSizeMax.Left)
	$DeltaY = [Math]::Abs(0 - $PowerShellSizeMax.Top)
	$DeltaW = [Math]::Abs($PowerShellSizeMax.Width - $ScreenSize.Width)
	$DeltaH = [Math]::Abs($PowerShellSizeMax.Height - $ScreenSize.Height)
	
	$ObjDelta = [PSCustomObject]@{
		DeltaX = $DeltaX
		DeltaY = $DeltaY
		DeltaW = $DeltaW
		DeltaH = $DeltaH
	}
	$ObjDelta.PSTypeNames.Insert(0, 'System.Automation.WindowDeltaInfo' )
	$ObjDelta
}


# Positioniert ein Applikationsfenster links oder rechts
# Als App-Referenz kann übergeben werden:
# - Das MainWindowHandle
# - Die ProcessID
# - Der ProcessName
Function Set-Window-LeftRight() {
	[CmdletBinding(SupportsShouldProcess)]
	Param (
		$WindowDeltas,
        [parameter(ValueFromPipelineByPropertyName)]
        $ProcessName = $Null,
		[System.Nullable[Int]]$ProcessID = $Null,
		[System.Nullable[Int]]$MainWindowHandle = $Null,
		[Switch]$PositionLeft,
		[Switch]$PositionRight
    )
	
	If ($PositionLeft -eq $False -and $PositionRight -eq $False) {
		Return
	}

	# Allenfalls das MainWindowHandle bestimmen
	If ($MainWindowHandle -eq $null -and [String]::IsNullOrEmpty($ProcessName) -and $ProcessID -eq $Null) {
		Log 4 'Set-Window-LeftRight(): Einer dieser Parameter muss definiert sein!:' -ForegroundColor Red
		Log 5 '-MainWindowHandle'
		Log 5 '-ProcessName'
		Log 5 '-ProcessID'
		Log 4 'Abbruch.'
		Return
	}
	
	If ($MainWindowHandle -eq $null) {
		$GetProcessSplat = @{}
		If (Has-Value $ProcessName) { $GetProcessSplat += @{ Name = $ProcessName} }
		If ($ProcessID -ne $Null) { $GetProcessSplat += @{ ID = $ProcessID} }
		$MainWindowHandle = (Get-Process @GetProcessSplat).MainWindowHandle
	}
	
	$ScreenSize = Get-Screen-Primary-WorkingArea
	
	# Neue Position berechnen
	If ($PositionLeft) {
		$NewPosX = 0 - $WindowDeltas.DeltaX
		$NewPosY = 0 - $WindowDeltas.DeltaY
		$NewPosR = ($ScreenSize.Width / 2) + $WindowDeltas.DeltaX
		$NewPosB = $ScreenSize.Height + $WindowDeltas.DeltaY
		$NewPosW = $NewPosR - $NewPosX
		$NewPosH = $NewPosB - $NewPosY
	}
	If ($PositionRight) {
		$NewPosX = ($ScreenSize.Width / 2) - $WindowDeltas.DeltaX
		$NewPosY = 0 - $WindowDeltas.DeltaY
		$NewPosR = $ScreenSize.Width  + $WindowDeltas.DeltaX
		$NewPosB = $ScreenSize.Height + $WindowDeltas.DeltaY
		$NewPosW = $NewPosR - $NewPosX
		$NewPosH = $NewPosB - $NewPosY
	}

	# Log 4 "NewPosX: $NewPosX"
	# Log 4 "NewPosY: $NewPosY"
	# Log 4 "NewPosW: $NewPosW"
	# Log 4 "NewPosH: $NewPosH"
	
	Set-Process-Window -MainWindowHandle $MainWindowHandle -X $NewPosX -Y $NewPosY -Width $NewPosW -Height $NewPosH
}


#Endregion Window Management


#Region RDP Tools

# Zeigt die angemeldeten User
# SessionName:
# 	console		Lokal
# 	rdp-tcp#10	RDP-Sitzung
Function Get-LoggedinUsers {
<#
.SYNOPSIS
	Shows the users currently logged into the specified computername if not specified local computers will be shown.
.INFO	
	!Q
	https://community.spiceworks.com/people/jitensh
#>
	[CmdletBinding()]
	Param(
		[String[]]$Computer = 'Localhost'
	)

	ForEach ($Comp in $Computer)  { 
		If (-not (Test-Connection -ComputerName $comp -Quiet -Count 1 -ea silentlycontinue))  {
			Write-Warning "$comp is Offline"; continue
		} 
		$stringOutput = quser /server:$Comp 2>$null
		If (!$stringOutput) {
			Write-Warning "Unable to retrieve quser info for `"$Comp`""
		}
		ForEach ($line in $stringOutput) {
			If ($line -match "logon time") { Continue }

			[PSCustomObject]@{
				ComputerName    = $Comp
				Username        = $line.SubString(1, 20).Trim()
				SessionName     = $line.SubString(23, 17).Trim()
				ID             = $line.SubString(42, 2).Trim()
				State           = $line.SubString(46, 6).Trim()
				#Idle           = $line.SubString(54, 9).Trim().Replace('+', '.')
				#LogonTime      = [datetime]$line.SubString(65)
			}
		} 
	} 
}


# Liefert true, wenn eine RDP-Sitzung aktiv ist
Function Is-RDP-Session-Active() {
	$SessionName = Get-LoggedinUsers | ? Username -eq $Env:USERNAME | Select -ExpandProperty SessionName
	If ([String]::IsNullOrEmpty($SessionName) -or $SessionName -eq 'console') {
		$False
	} Else {
		$True
	}
}

#Endregion RDP Tools



<#
.SYNOPSIS
    Runs the specified command in an elevated context.

.DESCRIPTION
    Runs the specified command in an elevated context. This is useful on Windows
    systems where the user account control is enabled. Input object and result
    objects are serialized using XML.
    It's important, the command does use the current user context. This means,
    the current user needs administrative permissions on the local system.
    If no file path or script block is specified, the current running Process
    will be run as administrator.

.OUTPUTS
    Output of the invoked script block or command.

.EXAMPLE
    Invoke-Elevated
    Will start the current Process, e.g. PowerShell Console or ISE, in an
    elevated session as Administrator.

.EXAMPLE
    Invoke-Elevated -FilePath 'C:\Temp\script.ps1'
    Start the script in an elevated session and return the result.

.EXAMPLE
    Invoke-Elevated -ScriptBlock { Get-DscLocalConfigurationManager }
    Start the script in an elevated session and return the result.

.EXAMPLE
    Invoke-Elevated -ScriptBlock { Param ($Path) Remove-Item -Path $Path } -ArgumentList 'C:\Windows\test.txt'
    Delete a file from the program files folder with elevated permission,
    beacuse a normal user account has no permissions.

.NOTES
    https://github.com/claudiospizzi/SecurityFever
	https://www.powershellgallery.com/packages/SecurityFever/1.0.2/Content/Functions%5CInvoke-Elevated.ps1
#>
Function Invoke-Elevated-PSScript {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '', Scope='Function', Target= 'Invoke-Elevated-PSScript')]
    [CmdletBinding(DefaultParameterSetName = 'None')]
    [Alias('sudo')]
    Param (
        # The path to a script
        [Parameter(Mandatory, Position = 0, ParameterSetName = 'ScriptFilePath')]
        [ValidateScript({Test-Path -Path $_})]
        [String]$ScriptFilePath,

        # The script block to execute in an elevated context.
        [Parameter(Mandatory, Position = 0, ParameterSetName = 'ScriptBlock')]
        [Management.Automation.ScriptBlock]$ScriptBlock,

        # Optional argument list for the program or the script block.
        [Parameter(Position = 1)]
        [Object[]]$ArgumentList
    )

	# tomtom
	$DebugInvokeElevated = $False

    If ($PSCmdlet.ParameterSetName -eq 'None') {
        # If no file path and script block was specified, just elevate the
        # current session for interactive use. For this, use the start info
        # object of the current Process and start an elevated new one.
        $CurrentProcess = Get-Process -Id $PID

        $ProcessStart = $CurrentProcess.StartInfo
        $ProcessStart.FileName         = $CurrentProcess.Path
        $ProcessStart.Verb             = 'RunAs'

        $Process = New-Object -TypeName Diagnostics.Process
        $Process.StartInfo = $ProcessStart
        $Process.Start() | Out-Null
    }

    If ($PSCmdlet.ParameterSetName -eq 'ScriptFilePath') {
        # If a file path instead of a script block was specified, just load the
        # file content and parse it as script block.
        $ScriptBlock = [Management.Automation.ScriptBlock]::Create((Get-Content -Path $ScriptFilePath -ErrorAction Stop -Raw))
    }

    If ($PSCmdlet.ParameterSetName -eq 'ScriptFilePath' -or $PSCmdlet.ParameterSetName -eq 'ScriptBlock') {
        Try {
            # To transport the parameters, script outputs and the errors, we use
            # the CliXml object serialization and temporary files. This is
            # necessary because the elevated Process runs in an elevated context
            $ScriptBlockFile   = [IO.Path]::GetTempFileName() + '.xml'
            $ArgumentListFile  = [IO.Path]::GetTempFileName() + '.xml'
            $CommandOutputFile = [IO.Path]::GetTempFileName() + '.xml'
            $CommandErrorFile  = [IO.Path]::GetTempFileName() + '.xml'

            $ScriptBlock  | Export-Clixml -Path $ScriptBlockFile
            $ArgumentList | Export-Clixml -Path $ArgumentListFile

            # Create a command string which contains all command executed in the
            # elevated session. The wrapper of the script block is needed to
            # pass the parameters and return all outputs objects and errors.
            $CommandString = ''
            $CommandString += 'Set-Location -Path "{0}";' -f $pwd.Path
            $CommandString += '$scriptBlock = [Management.Automation.ScriptBlock]::Create((Import-Clixml -Path "{0}"));' -f $ScriptBlockFile
            $CommandString += '$argumentList = [Object[]] (Import-Clixml -Path "{0}");' -f $ArgumentListFile
            $CommandString += '$output = Invoke-Command -ScriptBlock $scriptBlock -ArgumentList $argumentList;'
            $CommandString += '$error | Export-Clixml -Path "{0}";' -f $CommandErrorFile
            $CommandString += '$output | Export-Clixml -Path "{0}";' -f $CommandOutputFile

            $CommandEncoded = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($CommandString))

            $ProcessStart = New-Object -TypeName Diagnostics.ProcessStartInfo -ArgumentList 'C:\WINDOWS\System32\WindowsPowerShell\v1.0\powershell.exe'
            $ProcessStart.Arguments   = '-NoProfile -NonInteractive -EncodedCommand {0}' -f $CommandEncoded
            $ProcessStart.Verb        = 'RunAs'
            $ProcessStart.WindowStyle = 'Hidden'

            $Process = New-Object -TypeName Diagnostics.Process
            $Process.StartInfo = $ProcessStart
            $Process.Start() | Out-Null

            Write-Verbose "Elevated powershell.exe Process started with id $($Process.Id)."

            $Process.WaitForExit()

            Write-Verbose "Elevated powershell.exe Process stopped with exit code $($Process.ExitCode)."

            If ((Test-Path -Path $CommandErrorFile)) {
                Import-Clixml -Path $CommandErrorFile | ForEach-Object { Write-Error $_ }
            }

            If ((Test-Path -Path $CommandOutputFile)) {
                Import-Clixml -Path $CommandOutputFile | Write-Output
            }
        } Catch {
            Throw $_
        }
        Finally {
            If ($null -ne $Process) {
                $Process.Dispose()
            }

			If ($DebugInvokeElevated) {
				Write-Host "ScriptBlockFile: $ScriptBlockFile"
				Write-Host "ArgumentListFile: $ArgumentListFile"
				Write-Host "CommandOutputFile: $CommandOutputFile"
				Write-Host "CommandErrorFile: $CommandErrorFile"
			} Else {
				Remove-Item -Path $ScriptBlockFile   -Force -ErrorAction SilentlyContinue
				Remove-Item -Path $ArgumentListFile  -Force -ErrorAction SilentlyContinue
				Remove-Item -Path $CommandOutputFile -Force -ErrorAction SilentlyContinue
				Remove-Item -Path $CommandErrorFile  -Force -ErrorAction SilentlyContinue
			}
        }
    }
}


# Startet ein Exe - auch elevated
# Ex
# 	Invoke-Elevated -ExeFile $RevoUninstallerExe -Verb 'RunAs' -Args "/mu '$FoundAppName' /path '$FoundAppInstallDir' /mode Advanced /32"
Function Invoke-Exe {
	Param (
		[Parameter(Mandatory)]
		[ValidateScript({ Test-Path -Path $_ })]
		[String]$ExeFile,
		[String[]]$Args,
		[String]$Verb
	)
	
	$RunAs = $Verb -eq 'RunAs'
	# If ($RunAs) { Log 3 "RunAs ist aktiv, deshalb kein RedirectOutput!" Yellow }
	
	Try {
		$oPsi = New-Object -TypeName System.Diagnostics.ProcessStartInfo
		
		# tomtom: RunAs und Redirect funktionieren nicht gleichzeitig!
		If (! $RunAs) {
			$oPsi.CreateNoWindow = $true # Nicht relevant für RunAs, wird ignoriert
			$oPsi.UseShellExecute = $false # Blockiert RunAs
			$oPsi.RedirectStandardOutput = $true
			$oPsi.RedirectStandardError = $true
		}
		$oPsi.FileName = $ExeFile
		$oPsi.WindowStyle = 'Hidden'
		If (! [String]::IsNullOrEmpty($Args)) { $oPsi.Arguments = $Args }
		If (! [String]::IsNullOrEmpty($Verb)) { $oPsi.Verb = $Verb }
		
		# Create process object
		$oProcess = New-Object -TypeName System.Diagnostics.Process
		$oProcess.StartInfo = $oPsi
		
		If (! $RunAs) {
			# Create string builders to store stdout and stderr
			$oStdOutBuilder = New-Object -TypeName System.Text.StringBuilder
			$oStdErrBuilder = New-Object -TypeName System.Text.StringBuilder
			
			# Add event handers for stdout and stderr
			$sScripBlock = {
				If (! [String]::IsNullOrEmpty($EventArgs.Data)) {
					$Event.MessageData.AppendLine($EventArgs.Data)
				}
			}
			$oStdOutEvent = Register-ObjectEvent -InputObject $oProcess `
															 -Action $sScripBlock -EventName 'OutputDataReceived' `
															 -MessageData $oStdOutBuilder
			$oStdErrEvent = Register-ObjectEvent -InputObject $oProcess `
															 -Action $sScripBlock -EventName 'ErrorDataReceived' `
															 -MessageData $oStdErrBuilder
		}
		
		# Start the process
		[Void]$oProcess.Start()
		If (! $RunAs) {
			$oProcess.BeginOutputReadLine()
			$oProcess.BeginErrorReadLine()
		}
		[Void]$oProcess.WaitForExit()
		
		If (! $RunAs) {
			# Unregister events to retrieve process output
			Unregister-Event -SourceIdentifier $oStdOutEvent.Name
			Unregister-Event -SourceIdentifier $oStdErrEvent.Name
		}
		
		$Result = [Ordered]@{
			ExeFile = $ExeFile
			Args	  = $Args -join ' '
			Verb	  = $Verb
			ExitCode = $oProcess.ExitCode
		}
		If (! $RunAs) {
			$Result += [ordered]@{ StdOut = $oStdOutBuilder.ToString().Trim() }
			$Result += [ordered]@{ StdErr = $oStdErrBuilder.ToString().Trim() }
		}
		
		# New-Object -TypeName PSObject -Property $Result | FL
		Return $Result
		
	} Catch {
		Throw $_
	} Finally {
		If ($null -ne $oProcess) {
			$oProcess.Dispose()
		}
	}
}


# Liefert True, wenn das Script im Elevated Kontext läuft
Function Is-Script-Elevated() {
	[Bool](([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator))
}
# Liefert True, wenn das Script NICHT im Elevated Kontext läuft
Function IsNot-Script-Elevated() {
	(Is-Script-Elevated) -eq $False
}


# Korrigiert Fehler im Uninstall-String
# Korrigierte Fehler:
# - Der Pfad zum exe ist nicht mit " umgeben
Function Fix-UninstallString($UninstallString) {
	# Ist der Pfad zum mit Anführungszeichen umgeben?
	# suchen, wo wir '.exe' finden
	$Items = $UninstallString -Split ' '
	$ExeItemNo = $Null
	For ($idx = 0; $idx -lt $Items.Count; $idx++) {
		If ($Items[$idx].ToLower().EndsWith('.exe')) {
			$ExeItemNo = $idx
			Break;
		}
	}
	
	# Wenn das Exe nicht zum ersten Item gehört, haben wir einen String mit Leerzeichen
	If ($ExeItemNo -gt 0) {
		# Alle Items bis zum Element mit .exe wieder zusammenführen
		$FixedUninstallString = ("`"{0}`"" -f ($Items[0 .. $ExeItemNo] -join ' '))
		# Haben wir nach dem Item mit .exe weitere Elemente?
		If ($ExeItemNo -lt $Items.Count-1) {
			$CmdArgs = $Items[($ExeItemNo+1) .. ($Items.Count)] -join ' '
			$FixedUninstallString = ("{0} {1}" -f $FixedUninstallString, $CmdArgs)
		}
		Return $FixedUninstallString
	}
	
	Return $UninstallString
}


# Trennt von einem Uninstall-String das Programm 
# von den Argumenten
# $Program, $Arguments = Split-Programm-And-Arguments $UninstallString
Function Split-Programm-And-Arguments($UninstallString) {
	$ExeExt = '.exe'
	$ExePos = $UninstallString.ToLower().IndexOf($ExeExt)
	
	$CharNachExe = $UninstallString[$ExePos + $ExeExt.Length]
	Switch($CharNachExe) {
		( { @('"', "'") -Contains $_ } ) {
			# Ganz normal ein ' oder " nach dem .exe
			$Program = $UninstallString[0..($ExePos + $ExeExt.Length)] -join ''
			$Arguments = $UninstallString[($ExePos + $ExeExt.Length)..($UninstallString.Length)] -join ''
			Return @($Program, $Arguments)
		}
		Default {
			Write-Error "Unerwartetes Zeichen '$CharNachExe' nach .exe: $($UninstallString)"
		}
	}
}


# Stellt sicher, 
# - dass MSI-Uninstall-Strings nicht als App-Install/Konfig-Strings ausgeführt werden
# - dass ein Reboot wann immer möglich unterdrückt wird
Function Uninstall-Software-By-UninstallString() {
	[CmdletBinding(SupportsShouldProcess)]
	Param (
		[Parameter(Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
		[String]$UninstallString,
		[Parameter(Position = 1, ValueFromPipeline, ValueFromPipelineByPropertyName)]
		[String]$ProgramName, 
		[Switch]$ShowDebugInfo
	)

	If (Has-Value $ProgramName) { Log 2 "Deinstalliere: $ProgramName" }
	If ($ShowDebugInfo) { Log 3 "UninstallString: $UninstallString" }

	If ($UninstallString -like 'MsiEx*') {
		# Uninstall mit MSI
		$Items = $UninstallString -Split '\s+'
		$Command = $Items[0]
		# Dummerweise sind manche Uninstall-Strings im Endeffekt App-Install/Konfig-Strings
		# Drum /I mit /x ersetzen
		$Arg1 = $Items[1] -replace '/i','/x'
		If ($ShowDebugInfo) {
			Log 3 "Starte MSI:"
			Log 4 "$Command $($Arg1) /qn REBOOT=SUPPRESS"
		}
		Start-Process $Command -ArgumentList "$($Arg1) /qn REBOOT=SUPPRESS" -Wait
	} Else {
		# Uninstall ohne MSI
		If ($ShowDebugInfo) { Log 3 "UninstallString Ori: $UninstallString" }
		$FixedUninstallString = Fix-UninstallString $UninstallString
		If ($ShowDebugInfo) { Log 3 "UninstallString Bereinigt: $FixedUninstallString" }
		$Program, $Arguments = Split-Programm-And-Arguments $FixedUninstallString
		If ($ShowDebugInfo) { Log 3 "Starte Uninstall-String:" }
		If ($ShowDebugInfo) { Log 4 "Program: $Program" }
		If ($ShowDebugInfo) { Log 4 "Arguments: $Arguments" }
		Start-Process $Program -ArgumentList $Arguments -Wait
	}	
}


# Liefert die Liste der installierten SW
Function Get-Installed-Software {
<#
.Synopsis
	This function generates a list by querying the registry and returning the installed programs of a local or remote computer.
	200807, tom-agplv3@jig.ch

.PARAMETER ComputerName
	The computer to which connectivity will be checked

.PARAMETER Property
	Additional values to be loaded from the registry. 
	A String or array of String hat will be attempted to retrieve from the registry for each program entry

.PARAMETER IncludeProgram
	This will include the Programs matching that are specified as argument in this parameter. 
	Wildcards allowed. 

.PARAMETER ExcludeProgram
	This will exclude the Programs matching that are specified as argument in this parameter. 
	Wildcards allowed. 

.PARAMETER ProgramRegExMatch
	Change IncludeProgram / ExcludeProgram 
	from -like operator 
	to -match operator. 

.PARAMETER LastAccessTime
	Estimates the last time the program was executed by looking in the installation folder, 
	if it exists, and retrieves the most recent LastAccessTime attribute of any .exe in that folder. 
	This increases execution time of this script as it requires (remotely) querying the file system to retrieve this information.

.PARAMETER ExcludeSimilar
	This will filter out similar programnames, 
	the default value is to filter on the first 3 words in a program name. 
	If a program only consists of less words it is excluded and it will not be filtered. 
	For example if you Visual Studio 2015 installed it will list all the components individually, 
	using -ExcludeSimilar will only display the first entry.

.PARAMETER SimilarWord
	This parameter only works when ExcludeSimilar is specified, 
	it changes the default of first 3 words to any desired value.

.PARAMETER DisplayRegPath
	Displays the registry path as well as the program name

.PARAMETER MicrosoftStore
	Also queries the package list reg key, allows for listing Microsoft Store products for current user


.EXAMPLE
	Get-Installed-Software
	Get list of installed programs on local machine

.EXAMPLE
	Get-Installed-Software -Property DisplayVersion,VersionMajor,Installdate | ft

.EXAMPLE
	Get-Installed-Software -Property DisplayVersion,VersionMajor,Installdate,UninstallString

.EXAMPLE
	Get-Installed-Software -ComputerName server01,server02
	Get list of installed programs on server01 and server02

.EXAMPLE
	'server01','server02' | Get-Installed-Software -Property UninstallString
	Get the installed programs on server01/02 that are passed on to the function 
	through the pipeline 
	and also retrieves the uninstall String for each program

.EXAMPLE
	'server01','server02' | Get-Installed-Software -Property UninstallString -ExcludeSimilar -SimilarWord 4
	Get retrieve the installed programs on server01/02 
	that are passed on to the function through the pipeline 
	and also retrieves the uninstall String for each program. 
	Will only display a single entry of a program of which the first four words are identical.

.EXAMPLE
	Get-Installed-Software -Property installdate,UninstallString,installlocation -LastAccessTime | Where-Object {$_.installlocation}
	Get the list of programs from Server01 
	and retrieves the InstallDate,UninstallString and InstallLocation properties. 
	Then filters out all products that do not have a installlocation set 
	and displays the LastAccessTime when it can be resolved.

.EXAMPLE
	Get-Installed-Software -Property installdate -IncludeProgram *office*
	Get the InstallDate of all components that match the wildcard pattern of *office*

.EXAMPLE
	Get-Installed-Software -Property installdate -IncludeProgram 'Microsoft Office Access','Microsoft SQL Server 2014'

	Get the InstallDate of all components 
	that exactly match Microsoft Office Access & Microsoft SQL Server 2014

.EXAMPLE
	Get-Installed-Software -Property installdate -IncludeProgram '*[10*]*' | Format-Table -Autosize > MyInstalledPrograms.txt

	Get the ComputerName, ProgramName and installdate 
	of the programs matching the *[10*]* wildcard 
	and using Format-Table 
	and redirection to write this output to text file

.EXAMPLE
	Get-Installed-Software -IncludeProgram ^Office -ProgramRegExMatch

	Get the InstallDate of all components 
	that match the regex pattern of ^Office.*, 
	which means any ProgramName starting with the word Office

.EXAMPLE
	Get-Installed-Software -DisplayRegPath

	Get the list of programs from the local system and displays the registry path

.EXAMPLE
	Get-Installed-Software -DisplayRegPath -MicrosoftStore

.NOTES
	Q
	https://gallery.technet.microsoft.com/scriptcenter/Get-Installed-Software-Get-list-de9fd2b4
	001 O
	002	Bereinigung des Uninstall-Strings
#>
    [CmdletBinding(SupportsShouldProcess=$true)]
    Param(
        [Parameter(ValueFromPipeline              =$true,
                   ValueFromPipelineByPropertyName=$true,
                   Position=0
        )]
        [String[]]$ComputerName = $env:COMPUTERNAME,
        [Parameter(Position=0)]
        [String[]]$Property,
        [String[]]$IncludeProgram,
        [String[]]$ExcludeProgram,
        [switch]$ProgramRegExMatch,
        [switch]$LastAccessTime,
        [switch]$ExcludeSimilar,
        [switch]$DisplayRegPath,
        [switch]$MicrosoftStore,
        [Int]$SimilarWord
    )

    Begin {
        $RegistryLocation = 'SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\',
                            'SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\'

        if ($psversiontable.psversion.major -gt 2) {
            $HashProperty = [ordered]@{}    
        } else {
            $HashProperty = @{}
            $SelectProperty = @('ComputerName','ProgramName')
            if ($Property) {
                $SelectProperty += $Property
            }
            if ($LastAccessTime) {
                $SelectProperty += 'LastAccessTime'
            }
        }
    }

    Process {
        foreach ($Computer in $ComputerName) {
            try {
                $socket = New-Object Net.Sockets.TcpClient($Computer, 445)
                if ($socket.Connected) {
                    'LocalMachine', 'CurrentUser' | ForEach-Object {
                        $RegName = if ('LocalMachine' -eq $_) {
                            'HKLM:\'
                        } else {
                            'HKCU:\'
                        }

                        if ($MicrosoftStore) {
                            $MSStoreRegPath = 'Software\Classes\Local Settings\Software\Microsoft\Windows\CurrentVersion\AppModel\Repository\Packages\'
                            if ('HKCU:\' -eq $RegName) {
                                if ($RegistryLocation -notcontains $MSStoreRegPath) {
                                    $RegistryLocation = $MSStoreRegPath
                                }
                            }
                        }
                        
                        $RegBase = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey([Microsoft.Win32.RegistryHive]::$_,$Computer)
                        $RegistryLocation | ForEach-Object {
                            $CurrentReg = $_
                            if ($RegBase) {
                                $CurrentRegKey = $RegBase.OpenSubKey($CurrentReg)
                                if ($CurrentRegKey) {
                                    $CurrentRegKey.GetSubKeyNames() | ForEach-Object {
                                        Write-Verbose -Message ('{0}{1}{2}' -f $RegName, $CurrentReg, $_)

                                        $DisplayName = ($RegBase.OpenSubKey("$CurrentReg$_")).GetValue('DisplayName')
                                        if (($DisplayName -match '^@{.*?}$') -and ($CurrentReg -eq $MSStoreRegPath)) {
                                            $DisplayName = $DisplayName  -replace '.*?\/\/(.*?)\/.*','$1'
                                        }

                                        $HashProperty.ComputerName = $Computer
                                        $HashProperty.ProgramName = $DisplayName
                                        
                                        if ($DisplayRegPath) {
                                            $HashProperty.RegPath = '{0}{1}{2}' -f $RegName, $CurrentReg, $_
                                        } 

                                        if ($IncludeProgram) {
                                            if ($ProgramRegExMatch) {
                                                $IncludeProgram | ForEach-Object {
                                                    if ($DisplayName -notmatch $_) {
                                                        $DisplayName = $null
                                                    }
                                                }
                                            } else {
                                                $IncludeProgram | Where-Object {
                                                    $DisplayName -notlike ($_ -replace '\[','`[')
                                                } | ForEach-Object {
                                                        $DisplayName = $null
                                                }
                                            }
                                        }

                                        if ($ExcludeProgram) {
                                            if ($ProgramRegExMatch) {
                                                $ExcludeProgram | ForEach-Object {
                                                    if ($DisplayName -match $_) {
                                                        $DisplayName = $null
                                                    }
                                                }
                                            } else {
                                                $ExcludeProgram | Where-Object {
                                                    $DisplayName -like ($_ -replace '\[','`[')
                                                } | ForEach-Object {
                                                        $DisplayName = $null
                                                }
                                            }
                                        }

                                        if ($DisplayName) {
                                            if ($Property) {
                                                foreach ($CurrentProperty in $Property) {
													# tomtom: den UninstallString bereiigen
													If ($CurrentProperty -eq 'UninstallString') {
														$UninstallString = ($RegBase.OpenSubKey("$CurrentReg$_")).GetValue($CurrentProperty)
														$UninstallStringCln = $UninstallString
														If ([String]::IsNullOrEmpty($UninstallStringCln)) {
															$HashProperty.$CurrentProperty = ''
															$HashProperty.UninstallStringCln = ''
														} Else {
															$UninstallStringCln = $UninstallStringCln.Trim()
															# Äussere " entfernen
															If ($UninstallStringCln[0] -eq '"' -and $UninstallStringCln[-1] -eq '"') {
																$UninstallStringCln = $UninstallStringCln[1..($UninstallStringCln.Length-2)] -Join ''
															}
															# Äussere ' entfernen
															If ($UninstallStringCln[0] -eq "'" -and $UninstallStringCln[-1] -eq "'") {
																$UninstallStringCln = $UninstallStringCln[1..($UninstallStringCln.Length-2)] -Join ''
															}
															# Erzeugt:
															# "C:\Program Files (x86)\Cisco\Cisco AnyConnect Secure Mobility Client\Uninstall.exe" -remove
															$UninstallStringCln = Split-Command-AndArgs $UninstallStringCln
															$HashProperty.$CurrentProperty = $UninstallString
															$HashProperty.UninstallStringCln = $UninstallStringCln
														}
													} Else {
														$HashProperty.$CurrentProperty = ($RegBase.OpenSubKey("$CurrentReg$_")).GetValue($CurrentProperty)
													}
                                                }
                                            }
                                            if ($LastAccessTime) {
                                                $InstallPath = ($RegBase.OpenSubKey("$CurrentReg$_")).GetValue('InstallLocation') -replace '\\$',''
                                                if ($InstallPath) {
                                                    $WmiSplat = @{
                                                        ComputerName = $Computer
                                                        Query        = $("ASSOCIATORS OF {Win32_Directory.Name='$InstallPath'} Where ResultClass = CIM_DataFile")
                                                        ErrorAction  = 'SilentlyContinue'
                                                    }
                                                    $HashProperty.LastAccessTime = Get-WmiObject @WmiSplat |
                                                        Where-Object {$_.Extension -eq 'exe' -and $_.LastAccessed} |
                                                        Sort-Object -Property LastAccessed |
                                                        Select-Object -Last 1 | ForEach-Object {
                                                            $_.ConvertToDateTime($_.LastAccessed)
                                                        }
                                                } else {
                                                    $HashProperty.LastAccessTime = $null
                                                }
                                            }

                                            if ($psversiontable.psversion.major -gt 2) {
                                                [PSCustomObject]$HashProperty
                                            } else {
                                                New-Object -TypeName PSCustomObject -Property $HashProperty |
                                                Select-Object -Property $SelectProperty
                                            }
                                        }
                                        $socket.Close()
                                    }

                                }

                            }

                        }
                    }
                }
            } catch {
                Write-Error $_
            }
        }
    }
	
	End {
	}
	
}


# Erzeugt aus
# C:\Program Files (x86)\Cisco\Cisco AnyConnect Secure Mobility Client\Uninstall.exe -remove
# Den richtigen Befehl:
# "C:\Program Files (x86)\Cisco\Cisco AnyConnect Secure Mobility Client\Uninstall.exe" -remove
# 
Function Split-Command-AndArgs($Command) {
	$Items = $Command -split ' '
	for ($i=0; $i -lt $Items.Count; $i++) {
		$TestPath = $items[0 .. $i] -join ' '
		# Get-Command erkennt auch Befehle, ohne dass die Dateierweiterung angegeben wird :-)
		$Cmd = Get-Command $TestPath -ErrorAction SilentlyContinue
		If ($Cmd -ne $null) {
			Return ("`"{0}`" {1}" -f $TestPath, ($items[($i+1) .. ($Items.Count-1)] -join ' '))
		}
	}
}



#Region Chrome Browser

# Killt Chrome und löscht den Chrome-Browser Cache vom Default- und allen anderen Profilen
Function Delete-Chrome-Browser-Cache {
	Kill-Chrome-Browser
	
	$CacheItems = @(
		## Verzeichnisse
		'Cache'
		'Cache2\entries'
		## SQLite-Files
		'Cookies'
		'Cookies-journal'
		'History'
		'Archived History'
		'Top Sites'
		'VisitedLinks'
		'Web Data'
		'Media Cache'
		'Login Data'
		'Visited Links'
		## Ignoriert
		# 'ChromeDWriteFontCache'
		
		## Unklar, ob für uns nötig / gut:
		# 'Media History'
		# 'Favicons'
		# 'Network Action Predictor'
		# 'QuotaManager'
		# 'Reporting and NEL'
		# 'Shortcuts'
	)
	
	$DefaultProfileFolder = "$($env:LOCALAPPDATA)\Google\Chrome\User Data\Default"
	$CacheItems | % {
		If (Test-Path "$DefaultProfileFolder\$_") {
			Remove-Item "$DefaultProfileFolder\$_" -Force -Recurse -EA SilentlyContinue
		}
	}
	
	$OtherProfileFolders = Get-ChildItem "$($env:LOCALAPPDATA)\Google\Chrome\User Data" | ? { $_.PSIsContainer -and $_.Name -like "Profile*" }
	$OtherProfileFolders | % {
		$ProfilPath = $_.FullName
		$CacheItems | % {
			If (Test-Path "$ProfilPath\$_") {
				Remove-Item "$ProfilPath\$_" -Force -Recurse -EA SilentlyContinue
			}
		}
	}
}


# Sucht den Installationspfad des Chrome Browsers und
# liefert ein Objekt mit Informationen zun exe
# Wenn Chrome nicht installiert ist, wird $null zurückgegeben
# 
# Ex
# $ChromeInfo = Get-Chrome-Browser-Exe-Info
# $ChromeInfo | fl
#  OriginalFilename  : chrome.exe
#  FileDescription   : Google Chrome
#  ProductName       : Google Chrome
#  CompanyName       : Google LLC
#  FileName          : C:\Program Files (x86)\Google\Chrome\Application\chrome.exe
#  FileVersion       : 84.0.4147.125
#  ProductVersion    : 84.0.4147.125
#  FileVersionRaw    : 84.0.4147.125
#  ProductVersionRaw : 84.0.4147.125
Function Get-Chrome-Browser-Exe-Info {
	$RegAppPath = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe' -ErrorAction SilentlyContinue
	If ($RegAppPath -eq $Null) { Return $null }
	
	$RegAppPathDefault = Get-Item ($RegAppPath).'(Default)'
	If ($RegAppPathDefault -eq $Null) { Return $null }
	
	# Für den Fall, dass Google Chrome die Registry nicht bereinigt hat
	# Prüfen, ob das exe wirklich vorhanden ist
	If (Test-Path -LiteralPath ($RegAppPathDefault.VersionInfo).Filename -Type Leaf) {
		Return $RegAppPathDefault.VersionInfo
	}
	Return $null
}


# Wartet, bis Chrome.exe läuft und setzt es in den Vordergrund
Function Wait-For-Chrome-And-SetToForeground() {
	If ($ExeFile = Get-Chrome-Browser-Exe-Info) {
		Wait-For-ExeRunning-And-SetToForeground -ExeFile $ExeFile.FileName
	} Else {
		Log 4 "Der Chrome Webbroser wurde nicht gefunden!"
	}
}


# Startet den Chrome-Browser und öffnet die URL in einem neuen Fenster
Function Start-Chrome-Browser($Url, [Switch]$OpenNewWindow = $False) {
	# https://www.ghacks.net/list-useful-google-chrome-command-line-switches/
	# https://peter.sh/experiments/chromium-command-line-switches/#load-extension
	#  --incognito		Launches Chrome directly in Incognito private browsing mode
	If ($ChromeInfo = Get-Chrome-Browser-Exe-Info) {
		If ($OpenNewWindow) {
			. "$($ChromeInfo.Filename)" --new-window $Url
		} Else {
			. "$($ChromeInfo.Filename)" $Url
		}
	} Else {
		Log 4 "Der Chrome Webbroser wurde nicht gefunden!"
	}
}


# Killt den Chrome-Prozess
Function Kill-Chrome-Browser() {
	# Zuerst normal schliessen
	Get-Process | ? ProcessName -eq 'chrome' | Stop-Process
	Start-Sleep -Milliseconds 500
	# Sollte chrome sich weigern, wird er gekillt
	Get-Process | ? ProcessName -eq 'chrome' | Stop-Process -Force
	Start-Sleep -Milliseconds 500
}

# Installiert Chrome
Function Install-Chrome-Browser() {
	$SetupUrl = 'https://dl.google.com/chrome/install/latest/chrome_installer.exe'
	$Installer = ("$($env:TEMP)\{0}" -f 'chrome_installer.exe')
	Invoke-WebRequest $SetupUrl -OutFile $Installer
	# Start-Process -FilePath $Installer -Args '/silent /install' -Verb RunAs -Wait
	Start-Process -FilePath $Installer -Args '/install' -Verb RunAs -Wait
	Remove-Item $Installer -Force
	
	# Sollte Chrome automatisch starten, killen wir ihn wieder
	Start-Sleep -Milliseconds 1500
	Kill-Chrome-Browser
}


# Prüft, ob Chrome installiert ist und fragt, ob er allenfalls installiert werden soll
# Returns
#  $True		Chrome ist / wurde installiert 
#  $False	Chrome ist / wurde nicht installiert 
Function Is-Chrome-Installed() {
	Log 0 'Prüfe, ob Chrome installiert ist'
	If ($ChromeInfo = Get-Chrome-Browser-Exe-Info) {
		Return $True
	} Else {
		Log 1 'Der Chrome Webbroser wurde nicht gefunden!' -ForegroundColor Red
		$Answer = Wait-For-UserInput 'Der Chrome-Browser fehlt' 'Soll der Chrome-Browser installiert werden?' @('&Yes', '&Ja', '&No', '&Nein') 'Nein'
		If (@('Ja', 'Yes') -Contains $Answer) {
			Install-Chrome-Browser
			Start-Sleep -Milliseconds 1500
			Return $True
		}
	}
	Return $False
}


#Endregion Chrome Browser


# Gibt informationen zum Computer aus, um ein IT-Ticket zu machen
Function Print-Computer-Info {
	Log 0 'Das Gerät wurde bereits durch den Onboarding-Prozess erfasst' -ForegroundColor Yellow -BackgroundColor DarkRed
	Log 0 'und muss via Ticket aus der Cisco ISE Datenbank entfernt werden.' -ForegroundColor Yellow -BackgroundColor DarkRed
	
	Log 1 'Bitte ein Ticket mit etwa diesem Inhalt erstellen:' -ForegroundColor Yellow -BackgroundColor DarkRed
	Log 0 'Werter Support' -Append -NewLineBefore -ForegroundColor Cyan
	Log 0 ("Meine NPID ist: {0}" -f (Get-User-NPID)) -Append -ForegroundColor Cyan
	Log 0 'Beim Notebook-Onboarding erhielt ich diesen Fehler:' -Append -ForegroundColor Cyan
	Log 0 'Your device has already been added via the Nosergroup onboarding process. Please open a ticket to have it removed.' -Append -ForegroundColor Cyan
	Log 0 '' -Append -ForegroundColor Cyan
	Log 0 'Also, bitte entfernt es. Das sind die Koordinaten meines Notebooks:' -Append -ForegroundColor Cyan
	Log 0 "Domäne: $($env:userdomain)" -Append -ForegroundColor Cyan
	Log 0 "Benutzername: $($env:username)" -Append -ForegroundColor Cyan
	Log 0 "Hostname: $($env:computername)" -Append -ForegroundColor Cyan
	Log 0 'Netzwerk:' -Append -ForegroundColor Cyan
	(Get-NetworkConfig | Out-String) -split "`n" | % {
		Log 0 "$_" -Append -ForegroundColor Cyan
	}
	Log 0 'Vielen Dank für die rasche Erledigung,' -Append -ForegroundColor Cyan
	Log 0 'liebe Grüsse,' -Append -ForegroundColor Cyan
	Log 0 '' -Append
}


# Liefert die MAC-Adresse, den Namen und die IP-Adresse der NetworkAdapter
Function Get-NetworkConfig {
	$Blacklist = @('*Miniport*', '*Bluetooth*')
	$NetworkAdapters = Get-WmiObject Win32_NetworkAdapter
	$Res = ForEach ($NetworkAdapter in $NetworkAdapters) {
		If ($NetworkAdapter.MacAddress -eq $null) { Continue }
		If ($Blacklist | ? { $NetworkAdapter.Name -like $_ }) { Continue }
		$NetworkAdapter | % {
			$result = 1 | Select-Object MAC, Name, IP
			$result.Name = $_.Name
			$result.MAC = $_.MacAddress
			$config = $_.GetRelated('Win32_NetworkAdapterConfiguration') 
			$result.IP = ($config | Select-Object -expand IPAddress) -join "`n"
			$result
		}
	}
	$Res | FL
}


# Wartet auf eine Antwort vom Benutzer
# Wait-For-UserInput 'Titel' 'Bist Du bereit?' @('&Yes', '&Ja', '&No', '&Nein') 'Ja'
Function Wait-For-UserInput($Titel, $Msg, $Options, $Default) {
	
	# Optionen ohne '&'
	$OptionsArrTxt = $Options | % { $_.Replace('&','') }
	$DefaultIdx = $OptionsArrTxt.indexof($Default)
	$Response = $Host.UI.PromptForChoice($Titel, $Msg, $Options, $DefaultIdx)
	Return $OptionsArrTxt[$Response]
}


Function Show-Countdown($TimeoutSec = 1) {
	$Cnt = 0
	While ($Cnt -lt $TimeoutSec) {
		Write-Host ('.' * ($TimeoutSec-$Cnt) + ' ' * $TimeoutSec + "`r") -NoNewLine
		Start-Sleep -Seconds 1
		$Cnt++
	}
	Write-Host (" `r`n")
}
 

# Liefert das übergeordnete bin-Verzeichnis
Function Replace-FileExt($FileName, $NewFileExt) {
	[IO.Path]::Combine( `
		[IO.Path]::GetDirectoryName($FileName), `
		[IO.path]::GetFileNameWithoutExtension($FileName) + $NewFileExt
	)
}


# Weil Cisco ihre SW 'schützt', indem das exe nach dem Start gelöscht wird,
# wird halt eine Kopie gestartet
Function Start-Exe-Cloned-AndWait($ExeFile) {
	$TempExe = Replace-FileExt (New-TemporaryFile) '.exe'
	$Null = Copy-Item -LiteralPath $ExeFile -Destination $TempExe
	Start-Process $TempExe -Wait
}


Function Get-Bin-Dir() {
	$ScriptDir = Get-ScriptDir
	# $ParentDir = [IO.Path]::GetDirectoryName( [IO.Path]::GetDirectoryName( $ScriptDir ) )
	Return Join-Path $ScriptDir 'Bin'
}


# Sucht im Bin-Verzeichnis nach der neusten Datei 
# mit einem Suchmuster
# Ex $FileNameFilter
# 	'*anyconnect-ise-network-assistant-win*.exe'
Function Get-BinDir-Newest-File($FileNameFilter) {
	$BinDir = Get-Bin-Dir
	$FoundFiles = @(Get-ChildItem -LiteralPath $bindir -Filter $FileNameFilter | Sort LastWriteTime -Descending | select -First 1)
	If ($FoundFiles.Count -eq 0) {
		Log 1 "Im Bin-Verzeichnis:" -ForegroundColor Red
		Log 1 "$BinDir" -ForegroundColor Red
		Log 1 "wurde das File nicht gefunden:" -ForegroundColor Red
		Log 1 "$FileNameFilter" -ForegroundColor Red
		Log 1 "Abbruch." -ForegroundColor Red
		Break Script
	} Else {
		Return $FoundFiles.FullName
	}
}


# Sucht im Bin-Verzeichnis nach dem RevoUninstaller-Verzeichnis
Function Get-BinDir-RevoUninstallerDir() {
	$BinDir = Get-Bin-Dir
	
	$RevoUninstallerDir = @(Get-ChildItem -LiteralPath $BinDir -Include RevoUninstaller -Directory | select -First 1)
	If ($RevoUninstallerDir.Count -eq 0) {
		Log 1 "Im Bin-Verzeichnis:" -ForegroundColor Red
		Log 1 "$BinDir" -ForegroundColor Red
		Log 1 "wurde das RevoUninstaller Verzeichnis nicht gefunden" -ForegroundColor Red
		Log 1 "Abbruch." -ForegroundColor Red
		Break Script
	} Else {
		Return $RevoUninstallerDir[0].FullName
	}
}


# Sucht das RevoUninstaller Exe, das zur Plattform passt
# Ex
# 	Get-BinDir-RevoExe
#  C:\Temp\cisco-onboarding\Bin\RevoUninstallerPro-LicenseAkros\x64\RevoUnPro.exe
Function Get-BinDir-RevoUninstallerExe() {
	$RevoUninstallerDir = Get-BinDir-RevoUninstallerDir
	If (Is-OSx64) {
		$RevoUninstallerOSDir = Join-Path $RevoUninstallerDir 'x64'
	} Else {
		$RevoUninstallerOSDir = Join-Path $RevoUninstallerDir 'x86'
	}
	
	$FoundFiles = @(Get-ChildItem -LiteralPath $RevoUninstallerOSDir -Filter 'RevoUnPro.exe' | Sort LastWriteTime -Descending | select -First 1)
	If ($FoundFiles.Count -eq 0) {
		Log 1 "Im RevoUninstaller-Verzeichnis:" -ForegroundColor Red
		Log 1 "$RevoUninstallerOSDir" -ForegroundColor Red
		Log 1 "wurde das File nicht gefunden:" -ForegroundColor Red
		Log 1 "RevoUnPro.exe" -ForegroundColor Red
		Log 1 "Abbruch." -ForegroundColor Red
		Break Script
	} Else {
		Return $FoundFiles[0].FullName
	}
}


# Sucht das RevoUninstaller RevoCmd.exe, das zur Plattform passt
# RevoCmd.exe dient nur dazu, via Kommandozeile die Installations-Verzeichnisse und Uninstall-Strings zu suchen
# Ex
# 	Get-BinDir-RevoUninstaller-CmdExe
#  C:\Temp\cisco-onboarding\Bin\RevoUninstallerPro-LicenseAkros\x64\RevoCmd.exe
Function Get-BinDir-RevoUninstaller-CmdExe() {
	$RevoUninstallerDir = Get-BinDir-RevoUninstallerDir
	If (Is-OSx64) {
		$RevoUninstallerOSDir = Join-Path $RevoUninstallerDir 'x64'
	} Else {
		$RevoUninstallerOSDir = Join-Path $RevoUninstallerDir 'x86'
	}
	
	$FoundFiles = @(Get-ChildItem -LiteralPath $RevoUninstallerOSDir -Filter 'RevoCmd.exe' | Sort LastWriteTime -Descending | select -First 1)
	If ($FoundFiles.Count -eq 0) {
		Log 1 "Im RevoUninstaller-Verzeichnis:" -ForegroundColor Red
		Log 1 "$RevoUninstallerOSDir" -ForegroundColor Red
		Log 1 "wurde das File nicht gefunden:" -ForegroundColor Red
		Log 1 "RevoCmd.exe" -ForegroundColor Red
		Log 1 "Abbruch." -ForegroundColor Red
		Break Script
	} Else {
		Return $FoundFiles[0].FullName
	}
}



# Liefert $true, wenn wir ein x64 Windows haben
Function Is-OSx64() {
	switch ((Get-WmiObject -Class Win32_OperatingSystem).OSArchitecture -replace '\D') {
		64 {
			Return $True
		}
		default {
			Return $False
		}
	}
}


# Beendet das Script, forciert aber, dass der User die Fehlermeldung sieht
Function Stop-Script-Wait($WaitOnEnd) {
	If ($WaitOnEnd -eq $null -or $WaitOnEnd -eq 0) {
		# 10s warten, wenn nichts definiert ist
		Stop-Script 10
	} Else {
		Stop-Script $WaitOnEnd
	}
}

# Stoppt das Script, allenfalls mit einer Benutzerbestätigung, mit einem Timeout oder einem sofortigen Abbruch
# $WaitOnEnd
# 0    Script sofort beenden
# 1    Script nach einer Benutzerbestätigung beenden
# >1   Script nach einem Timeout mit $WaitOnEnd Sekunden beenden
# 200807
Function Stop-Script($WaitOnEnd) {
	If ($WaitOnEnd -eq $Null) { $WaitOnEnd = $Script:WaitOnEnd}
	
	Switch ($WaitOnEnd) {
		0 {
			# Nichts zu tun, Script beenden
		}
		1 {
			# Pause
			Log 0 'Press any key to continue …'
			Try { $x = $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") } Catch {}
		}
		Default {
			# Start-Sleep -Seconds $WaitOnEnd
			Show-Countdown $WaitOnEnd
		}
	}
	Break Script
}


# Re-Init-Tools -Debug $True
Function Re-Init-Tools($Debug = $False) {
	# C#1
	# $SecurePassword definieren und allenfalls das Plaintext PW konvertieren
	If (Has-Value $Script:Password) {
		If ($Script:Password.GetType().Name -eq 'SecureString') {
			If ($Debug) { Log 4 '$Password ist ein SecureString' }
			$Script:SecurePassword = $Script:Password
		} Else {
			If ($Debug) { Log 4 "Password: $Script:Password" }
			$Script:SecurePassword = ConvertTo-SecureString $Script:Password -AsPlainText -Force
		}
	} Else {
		If ($Debug) { Log 4 '$Password ist nicht definiert' }
	}


	# C#2
	# Allenfalls die NPID ergänzen und $NpidFqdn erzeugen
	If (Has-Value $Script:NPID) {
		If ($Debug) { Log 4 "NPID: $Script:NPID" }
		If ($Script:NPID.Contains($Script:NpidSuffix)) {
			$Script:NpidFqdn = $Script:NPID
		} Else {
			$Script:NpidFqdn = ("{0}{1}" -f $Script:NPID, $Script:NpidSuffix)
		}
		If ($Debug) { Log 4 "NpidFqdn: $Script:NpidFqdn" }
	} Else {
		If ($Debug) { Log 4 '$NPID ist nicht definiert' }
	}
}



Function Invoke-Executable {
<#
.SYNOPSIS
	Startet eine ausführbare Datei und liefert StdOut, StdErr und den ExitCode
	Returns: custom object
.NOTES   
	004, tom-agplv3@jig.ch, Jehoschua
	https://stackoverflow.com/questions/24370814/how-to-capture-process-output-asynchronously-in-powershell
	005
		Siehe: 				c:\Scripts\PowerShell\Test-RedirectStdIO\
		Änderung gemäss:	c:\Scripts\PowerShell\Test-RedirectStdIO\Test-Redirection.ps1
		Q: https://gist.github.com/jberezanski/f329ec1ccdf9c7dfb5ae 
.EXAMPLE
	$oResult = Invoke-Executable -sExeFile $cmd -cArgs @('8.8.8.8', '-a')
.EXAMPLE
	Invoke-Exe -ExeFile 'cmd' -Arguments @('/c dir')
	Invoke-Executable $exe $MyArgs
.EXAMPLE
	$exe = 'a:\Scripts\PowerShell\-Audio\lib\ffprobe.exe'
	$AudioFileName = 'c:\…\a.mp3'
	$MyArgs = @"
		-show_entries format=duration -v quiet -of csv="p=0" -i "$AudioFileName"
	"@
	Invoke-Executable $exe $MyArgs
.EXAMPLE
	$exe = 'a:\Scripts\PowerShell\-Audio\lib\ffprobe.exe'
	$AudioFileName = 'c:\…\a.mp3'
	$MyArgs=('-show_entries', 'format=duration', '-v quiet', '-of csv="p=0"', "-i `"$AudioFileName`"")
	Invoke-Executable $exe $MyArgs
#>
	[CmdletBinding(SupportsShouldProcess = $true)]
	Param (
		[Parameter(Position = 0, Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, HelpMessage = 'Die ausführbare Datei')]
		[ValidateNotNullOrEmpty()]
		[String]$ExeFile,
		
		[Parameter(Position = 1, ValueFromPipeline, ValueFromPipelineByPropertyName, HelpMessage = 'Die Argumente als String Array')]
		[String[]]$Arguments,
		
		[Parameter(Position = 2, ValueFromPipeline, ValueFromPipelineByPropertyName, HelpMessage = 'Das Arbeitsverzeichnis')]
		[string]$WorkingDirectory,
		
		[Parameter(Position = 3, ValueFromPipeline, ValueFromPipelineByPropertyName, HelpMessage = 'Das auzuführende Verb')]
		[String]$Verb,
		
		[Parameter(Position = 4, ValueFromPipeline, ValueFromPipelineByPropertyName, HelpMessage = 'Wenn aktiviert, werden die Argumente nicht auf potentielle Fehler geprüft')]
		[switch]$NoArgsCheck
	)
	Begin {
		$StdOut = New-Object System.Collections.ArrayList
		$StdErr = New-Object System.Collections.ArrayList
		
		If ($script:ThisModuleLoaded -eq $true) {
			Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
		}
		$FunctionName = $MyInvocation.MyCommand.Name
		Write-Verbose "$($FunctionName): Begin."
		
		If (! $NoArgsCheck) {
			$ArgsStr = $Arguments -join ' '
			If ($ArgsStr -match "`'") {
				Write-Warning "Invoke-Executable(): Disable this warning by using -NoArgsCheck:"
				Write-Warning "Betrifft: $ArgsStr"
				Write-Warning "Pfade sollten nicht mit ' sondern mit "" abgegrenzt werden!"
			}
		}
	}
	Process {
		# Setting process invocation parameters.
		$oPsi = New-Object System.Diagnostics.ProcessStartInfo
		Try {
			$oPsi.CreateNoWindow = $true
			$oPsi.UseShellExecute = $false
			$oPsi.RedirectStandardOutput = $true
			$oPsi.RedirectStandardError = $true
			$oPsi.FileName = $ExeFile
			If ($WorkingDirectory) {
				$oPsi.WorkingDirectory = $WorkingDirectory
			}
			
			If (! [String]::IsNullOrEmpty($Arguments)) { $oPsi.Arguments = $Arguments }
			If (! [String]::IsNullOrEmpty($Verb)) { $oPsi.Verb = $Verb }
			
			# Creating process object.
			$oProcess = New-Object System.Diagnostics.Process
			$oProcess.StartInfo = $oPsi
			[Void]$oProcess.Start()
			
			$errorReadTask = $oProcess.StandardError.ReadToEndAsync()
			$StdOut = $oProcess.StandardOutput.ReadToEnd()
			
			[Void]$oProcess.WaitForExit()
			$StdErr = $errorReadTask.Result
			$ExitCode = $oProcess.ExitCode
			
		} Finally {
			If ($oProcess) { $oProcess.Dispose() }
		}
		
		$oResult = New-Object -TypeName PSObject -Property ([Ordered]@{
				ExeFile = $ExeFile;
				Args	  = $Arguments -join " ";
				ExitCode = $ExitCode # $oProcess.ExitCode;
				StdOut  = $StdOut
				StdErr  = $StdErr
			})
		
		Return $oResult
	}
	End {
		Write-Verbose "$($FunctionName): End."
	}
}



## Noser-Spezifische Funktionen

#Region Noser-Spezifische Funktionen

# Liefert den Namen der Noser-Firma, die den Computer besitzt
# Resultat ist eine Enum vom Typ eNoserFirma
# 
# Diese Abfrage funktioniert:
# 	If ( (Get-Computer-Owner) -eq [eNoserFirma]::Unknown) { } 
Function Get-Computer-Owner() {
	
	# Akros: ANBxxxx.* (z.B. ANB123-Testautomation)
	$RgxAkrosHostname = 'ANB\d{3}.*'
	
	# NoE: NXXX (z.B. N527)
	$RgxNoeHostname = 'N\d{3}'
	
	# FX: PCXXX (z.B. PC5142)
	$RgxFxHostname = 'PC\d{4}'
	
	# NYP: nnb (z.B. nnb001)
	$RgxNypHostname = 'nnb\d{3}'
	
	If ($env:computername -match $RgxNoeHostname) {
		Return [eNoserFirma]::NoserEngineering
	}
	If ($env:computername -match $RgxFxHostname) {
		Return [eNoserFirma]::Frox
	}
	If ($env:computername -match $RgxAkrosHostname) {
		Return [eNoserFirma]::Akros
	}
	If ($env:computername -match $RgxNypHostname) {
		Return [eNoserFirma]::Nyp
	}
	Return [eNoserFirma]::Unknown
}


#Endregion Noser-Spezifische Funktionen


## Revo Uninstaller / RevoUninstaller

# Sucht von einer Applikation
# mit dem RevoUninstaller den exakten Applikationsnamen und das Installationsverzeichnis
# 
# Ex
# $FoundAppName, $FoundAppInstallDir = Get-RevoUninstaller-ApplicatonDir 'Cisco AnyConnect Secure Mobility Client'
Function Get-RevoUninstaller-ApplicatonDir($ApplicationName) {
	If ([String]::IsNullOrEmpty($ApplicationName)) {
		# Nichts gefunden
		Return @($Null, $Null)
	}
	
	
	$RevoUninstallerCmdExe = Get-BinDir-RevoUninstaller-CmdExe
	
	### Das Installationsverzeichnis suchen
	$Res = . $RevoUninstallerCmdExe /m "*$ApplicationName*" /i
	
	# Die erste ungerade Zeile des Resultats, das nicht leer ist, hat das Installationsverzeichnis
	For ($i = 0; $i -lt $Res.length; $i = $i + 2) {
		# Write-Host $Res[$i]
		# Write-Host $Res[$i+1]
		$FoundAppName = $Res[$i]
		$FoundAppInstallDir = $Res[$i + 1].TrimEnd("\")
		# Haben wir ein Installationsverzeichnis gefunden?
		If ([String]::IsNullOrEmpty($FoundAppInstallDir) -eq $false) {
			# $AppInstallDir = $Res[$i].TrimEnd("\")
			Break
		}
	}
	
	If ([String]::IsNullOrEmpty($FoundAppInstallDir)) {
		# Nicht gefunden
		Return @($Null, $Null)
	}
	
	Return @($FoundAppName, $FoundAppInstallDir)
}


# Löscht Leichen in der Registry und im Dateiverzeichnis, die die Cisco-Deinstallation nicht weggeputzt hat
# Es wird der Advanced-Mode des RevoUninstallers genützt
# 
# Ex
# 	Start-RevoUninstaller-Advanced-Wipe2 'Cisco AnyConnect Secure Mobility Client' 'C:\Program Files (x86)\Cisco\Cisco AnyConnect Secure Mobility Client'
Function Start-RevoUninstaller-Advanced-Wipe($ApplicationName, $ApplicationInstallDir) {
	If ([String]::IsNullOrEmpty($ApplicationName) -or [String]::IsNullOrEmpty($ApplicationInstallDir)) {
		# Nichts zu tun
		Return
	}
	
	$RevoUninstallerExe = Get-BinDir-RevoUninstallerExe
	$Cmd = @"
	"$RevoUninstallerExe" 
"@.Trim()
	
	# $null = Invoke-Exe -ExeFile $RevoUninstallerExe -Verb 'RunAs' -Args "/mu '$ApplicationName' /path '$ApplicationInstallDir' /mode Advanced /32"
	$CmdArgs = @"
		/mu "$ApplicationName" /path "$ApplicationInstallDir" /mode Advanced /32
"@.Trim()
	
	$Res = Invoke-Executable -ExeFile $Cmd -Arguments $CmdArgs -NoArgsCheck
	# $Res
	# Return ($Res.ExitCode -eq 0)
}


# Diese PowerShell-Variante Funktioniert nicht
# Löscht Leichen in der Registry und im Dateiverzeichnis, die die Cisco-Deinstallation nicht weggeputzt hat
# 
# Es wird der Advanced-Mode des RevoUninstallers genützt
Function Start-RevoUninstaller-Advanced-Wipe_001($ApplicationName, $ApplicationInstallDir) {
	If ([String]::IsNullOrEmpty($ApplicationName) -or [String]::IsNullOrEmpty($ApplicationInstallDir)) {
		# Nichts zu tun
		Return
	}
	
	$RevoUninstallerExe = Get-BinDir-RevoUninstallerExe
	# Log 4 "RevoUninstallerExe: $RevoUninstallerExe"
	# Invoke-Elevated-PSScript -ScriptFilePath $RevoUninstallerExe -ArgumentList "/mu '$ApplicationName' /path '$ApplicationInstallDir' /mode Advanced /32"
	$null = Invoke-Exe -ExeFile $RevoUninstallerExe -Verb 'RunAs' -Args "/mu '$ApplicationName' /path '$ApplicationInstallDir' /mode Advanced /32"
}


## Prepare

Re-Init-Tools


# SIG # Begin signature block
# MIImxAYJKoZIhvcNAQcCoIImtTCCJrECAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB
# gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR
# AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUfg/5ItL7yoHXtJ/Qa+CHqWtR
# 3pqggh/VMIIFbzCCBFegAwIBAgIQSPyTtGBVlI02p8mKidaUFjANBgkqhkiG9w0B
# AQwFADB7MQswCQYDVQQGEwJHQjEbMBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVy
# MRAwDgYDVQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEh
# MB8GA1UEAwwYQUFBIENlcnRpZmljYXRlIFNlcnZpY2VzMB4XDTIxMDUyNTAwMDAw
# MFoXDTI4MTIzMTIzNTk1OVowVjELMAkGA1UEBhMCR0IxGDAWBgNVBAoTD1NlY3Rp
# Z28gTGltaXRlZDEtMCsGA1UEAxMkU2VjdGlnbyBQdWJsaWMgQ29kZSBTaWduaW5n
# IFJvb3QgUjQ2MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAjeeUEiIE
# JHQu/xYjApKKtq42haxH1CORKz7cfeIxoFFvrISR41KKteKW3tCHYySJiv/vEpM7
# fbu2ir29BX8nm2tl06UMabG8STma8W1uquSggyfamg0rUOlLW7O4ZDakfko9qXGr
# YbNzszwLDO/bM1flvjQ345cbXf0fEj2CA3bm+z9m0pQxafptszSswXp43JJQ8mTH
# qi0Eq8Nq6uAvp6fcbtfo/9ohq0C/ue4NnsbZnpnvxt4fqQx2sycgoda6/YDnAdLv
# 64IplXCN/7sVz/7RDzaiLk8ykHRGa0c1E3cFM09jLrgt4b9lpwRrGNhx+swI8m2J
# mRCxrds+LOSqGLDGBwF1Z95t6WNjHjZ/aYm+qkU+blpfj6Fby50whjDoA7NAxg0P
# OM1nqFOI+rgwZfpvx+cdsYN0aT6sxGg7seZnM5q2COCABUhA7vaCZEao9XOwBpXy
# bGWfv1VbHJxXGsd4RnxwqpQbghesh+m2yQ6BHEDWFhcp/FycGCvqRfXvvdVnTyhe
# Be6QTHrnxvTQ/PrNPjJGEyA2igTqt6oHRpwNkzoJZplYXCmjuQymMDg80EY2NXyc
# uu7D1fkKdvp+BRtAypI16dV60bV/AK6pkKrFfwGcELEW/MxuGNxvYv6mUKe4e7id
# FT/+IAx1yCJaE5UZkADpGtXChvHjjuxf9OUCAwEAAaOCARIwggEOMB8GA1UdIwQY
# MBaAFKARCiM+lvEH7OKvKe+CpX/QMKS0MB0GA1UdDgQWBBQy65Ka/zWWSC8oQEJw
# IDaRXBeF5jAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zATBgNVHSUE
# DDAKBggrBgEFBQcDAzAbBgNVHSAEFDASMAYGBFUdIAAwCAYGZ4EMAQQBMEMGA1Ud
# HwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwuY29tb2RvY2EuY29tL0FBQUNlcnRpZmlj
# YXRlU2VydmljZXMuY3JsMDQGCCsGAQUFBwEBBCgwJjAkBggrBgEFBQcwAYYYaHR0
# cDovL29jc3AuY29tb2RvY2EuY29tMA0GCSqGSIb3DQEBDAUAA4IBAQASv6Hvi3Sa
# mES4aUa1qyQKDKSKZ7g6gb9Fin1SB6iNH04hhTmja14tIIa/ELiueTtTzbT72ES+
# BtlcY2fUQBaHRIZyKtYyFfUSg8L54V0RQGf2QidyxSPiAjgaTCDi2wH3zUZPJqJ8
# ZsBRNraJAlTH/Fj7bADu/pimLpWhDFMpH2/YGaZPnvesCepdgsaLr4CnvYFIUoQx
# 2jLsFeSmTD1sOXPUC4U5IOCFGmjhp0g4qdE2JXfBjRkWxYhMZn0vY86Y6GnfrDyo
# XZ3JHFuu2PMvdM+4fvbXg50RlmKarkUT2n/cR/vfw1Kf5gZV6Z2M8jpiUbzsJA8p
# 1FiAhORFe1rYMIIGGjCCBAKgAwIBAgIQYh1tDFIBnjuQeRUgiSEcCjANBgkqhkiG
# 9w0BAQwFADBWMQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVk
# MS0wKwYDVQQDEyRTZWN0aWdvIFB1YmxpYyBDb2RlIFNpZ25pbmcgUm9vdCBSNDYw
# HhcNMjEwMzIyMDAwMDAwWhcNMzYwMzIxMjM1OTU5WjBUMQswCQYDVQQGEwJHQjEY
# MBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMSswKQYDVQQDEyJTZWN0aWdvIFB1Ymxp
# YyBDb2RlIFNpZ25pbmcgQ0EgUjM2MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIB
# igKCAYEAmyudU/o1P45gBkNqwM/1f/bIU1MYyM7TbH78WAeVF3llMwsRHgBGRmxD
# eEDIArCS2VCoVk4Y/8j6stIkmYV5Gej4NgNjVQ4BYoDjGMwdjioXan1hlaGFt4Wk
# 9vT0k2oWJMJjL9G//N523hAm4jF4UjrW2pvv9+hdPX8tbbAfI3v0VdJiJPFy/7Xw
# iunD7mBxNtecM6ytIdUlh08T2z7mJEXZD9OWcJkZk5wDuf2q52PN43jc4T9OkoXZ
# 0arWZVeffvMr/iiIROSCzKoDmWABDRzV/UiQ5vqsaeFaqQdzFf4ed8peNWh1OaZX
# nYvZQgWx/SXiJDRSAolRzZEZquE6cbcH747FHncs/Kzcn0Ccv2jrOW+LPmnOyB+t
# AfiWu01TPhCr9VrkxsHC5qFNxaThTG5j4/Kc+ODD2dX/fmBECELcvzUHf9shoFvr
# n35XGf2RPaNTO2uSZ6n9otv7jElspkfK9qEATHZcodp+R4q2OIypxR//YEb3fkDn
# 3UayWW9bAgMBAAGjggFkMIIBYDAfBgNVHSMEGDAWgBQy65Ka/zWWSC8oQEJwIDaR
# XBeF5jAdBgNVHQ4EFgQUDyrLIIcouOxvSK4rVKYpqhekzQwwDgYDVR0PAQH/BAQD
# AgGGMBIGA1UdEwEB/wQIMAYBAf8CAQAwEwYDVR0lBAwwCgYIKwYBBQUHAwMwGwYD
# VR0gBBQwEjAGBgRVHSAAMAgGBmeBDAEEATBLBgNVHR8ERDBCMECgPqA8hjpodHRw
# Oi8vY3JsLnNlY3RpZ28uY29tL1NlY3RpZ29QdWJsaWNDb2RlU2lnbmluZ1Jvb3RS
# NDYuY3JsMHsGCCsGAQUFBwEBBG8wbTBGBggrBgEFBQcwAoY6aHR0cDovL2NydC5z
# ZWN0aWdvLmNvbS9TZWN0aWdvUHVibGljQ29kZVNpZ25pbmdSb290UjQ2LnA3YzAj
# BggrBgEFBQcwAYYXaHR0cDovL29jc3Auc2VjdGlnby5jb20wDQYJKoZIhvcNAQEM
# BQADggIBAAb/guF3YzZue6EVIJsT/wT+mHVEYcNWlXHRkT+FoetAQLHI1uBy/YXK
# ZDk8+Y1LoNqHrp22AKMGxQtgCivnDHFyAQ9GXTmlk7MjcgQbDCx6mn7yIawsppWk
# vfPkKaAQsiqaT9DnMWBHVNIabGqgQSGTrQWo43MOfsPynhbz2Hyxf5XWKZpRvr3d
# MapandPfYgoZ8iDL2OR3sYztgJrbG6VZ9DoTXFm1g0Rf97Aaen1l4c+w3DC+IkwF
# kvjFV3jS49ZSc4lShKK6BrPTJYs4NG1DGzmpToTnwoqZ8fAmi2XlZnuchC4NPSZa
# PATHvNIzt+z1PHo35D/f7j2pO1S8BCysQDHCbM5Mnomnq5aYcKCsdbh0czchOm8b
# kinLrYrKpii+Tk7pwL7TjRKLXkomm5D1Umds++pip8wH2cQpf93at3VDcOK4N7Ew
# oIJB0kak6pSzEu4I64U6gZs7tS/dGNSljf2OSSnRr7KWzq03zl8l75jy+hOds9TW
# SenLbjBQUGR96cFr6lEUfAIEHVC1L68Y1GGxx4/eRI82ut83axHMViw1+sVpbPxg
# 51Tbnio1lB93079WPFnYaOvfGAA0e0zcfF/M9gXr+korwQTh2Prqooq2bYNMvUoU
# KD85gnJ+t0smrWrb8dee2CvYZXD5laGtaAxOfy/VKNmwuWuAh9kcMIIGVzCCBL+g
# AwIBAgIRAJerP2s13dPOpevRLwsJhzgwDQYJKoZIhvcNAQEMBQAwVDELMAkGA1UE
# BhMCR0IxGDAWBgNVBAoTD1NlY3RpZ28gTGltaXRlZDErMCkGA1UEAxMiU2VjdGln
# byBQdWJsaWMgQ29kZSBTaWduaW5nIENBIFIzNjAeFw0yMjAyMjIwMDAwMDBaFw0y
# NTAyMjEyMzU5NTlaMEIxCzAJBgNVBAYTAkNIMQ0wCwYDVQQIDARCZXJuMREwDwYD
# VQQKDAhBS1JPUyBBRzERMA8GA1UEAwwIQUtST1MgQUcwggIiMA0GCSqGSIb3DQEB
# AQUAA4ICDwAwggIKAoICAQCpEHaWk6un+cfauC+UdCaWrIgc5OZFQIVVqo+wXffW
# GxD1tltMGe4tOmB2Vt+zCQl2pzJ0NZgMtO8a39023XuM0iPbp0o3OjEsWbqP4o2I
# HlJq5vhgn2cJpVDo16bwmbLywtnyNr9V2MRIh+Bil2/BeMPq+F9VHfiDaq6l0w5M
# BkOiNDSqNda+B79ShSoYcsGsYvLQfhFF5Pyh6a46SCtp/1AIaRVw3jnNJtocunCF
# ni7kb1UJP2Q16TEPo3lCxicyZWK0nof+o9KDmUOC84Tj6eat6dodXsW4vyOhoEYZ
# N2CprNBq7Q5crwWOTKNwz3+vNEOmlQlCqSIlrIKOFzBzDp3U2o9to+I8A9rsqY63
# BDrJFVq5KKe2DBRYwBZ5e7B9+MqS0Ug930xZBSDHQQ3DvvOEQgF7fKRQt1/X1g8T
# GO37MmN6sHd31OeM1kbx8lMM4f+DqegGD7IB6N8wKgnOazelyQXni38APNi/fpFA
# xixsFldvmI4Z8QoYqXsEiLDD05JKhUqpgITpRd8gLUiBuzOWs58FEXhx3X1C37iT
# TgyhBP7v4PUWWqe8KWukSPImE4J7y+1cJ7GqOPSLwkLgTuNm/Ko7wJyWfD4/LPNb
# O0a9YEHUTqbHoKIy55sIl7cmaaia5uz1Uzr3hzSVoVu1TwJl9qHdTQOM8/YnG7wQ
# 5wIDAQABo4IBtDCCAbAwHwYDVR0jBBgwFoAUDyrLIIcouOxvSK4rVKYpqhekzQww
# HQYDVR0OBBYEFBm6ziTh2H5Wi50vwBGP3n71Vh8FMA4GA1UdDwEB/wQEAwIHgDAM
# BgNVHRMBAf8EAjAAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMBEGCWCGSAGG+EIBAQQE
# AwIEEDBKBgNVHSAEQzBBMDUGDCsGAQQBsjEBAgEDAjAlMCMGCCsGAQUFBwIBFhdo
# dHRwczovL3NlY3RpZ28uY29tL0NQUzAIBgZngQwBBAEwSQYDVR0fBEIwQDA+oDyg
# OoY4aHR0cDovL2NybC5zZWN0aWdvLmNvbS9TZWN0aWdvUHVibGljQ29kZVNpZ25p
# bmdDQVIzNi5jcmwweQYIKwYBBQUHAQEEbTBrMEQGCCsGAQUFBzAChjhodHRwOi8v
# Y3J0LnNlY3RpZ28uY29tL1NlY3RpZ29QdWJsaWNDb2RlU2lnbmluZ0NBUjM2LmNy
# dDAjBggrBgEFBQcwAYYXaHR0cDovL29jc3Auc2VjdGlnby5jb20wFgYDVR0RBA8w
# DYELaXRAYWtyb3MuY2gwDQYJKoZIhvcNAQEMBQADggGBAFGyk9DeEOqnhYFNzxZ7
# HrS40+MelZjeyazVlzG5NfBIzxse6fpiP9C6f0zL//HrrjSkl9UyFNmn+5TfL/kw
# N40hogdDkna7DrsKxLQ1viBFLdJBxjg8Nz0KVgtcbUpi1Qj3H6UI4skLdUSir1iE
# oSR+QGkNzSJSrn2k8jijSMOYldbngzG+D9PzSNp/CjBCA1bTOBepuHVQQCl/5rY1
# bygvNL7Zua/Ca6iuxnAsJOisw9IkngKi3iznXTHQep0ytOOOvZhr3IRkOccy/cyz
# 9KA1jv68aKCsLiGpyUKCg8WqmwzLXCUfqxvouhrHr1yAYn2qHRRoQhiIw8TgghlE
# +PA9qKY6KIlmilX7VejuFScmrBQVkP0OuxLTufXIahY2jnPQfrJLz0U+fUL1YgHa
# 1oi+1rOZ/Z0iX2YXa1rDmrXueN8QNOtZuBEKBKUNSSq8hhH4rk2j7dVcYm1gljkd
# CR8vwGRBoj9oMJgehkTdMU9GAGu9Ny9COnT/J9mRc323HzCCBuwwggTUoAMCAQIC
# EDAPb6zdZph0fKlGNqd4LbkwDQYJKoZIhvcNAQEMBQAwgYgxCzAJBgNVBAYTAlVT
# MRMwEQYDVQQIEwpOZXcgSmVyc2V5MRQwEgYDVQQHEwtKZXJzZXkgQ2l0eTEeMBwG
# A1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMS4wLAYDVQQDEyVVU0VSVHJ1c3Qg
# UlNBIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTE5MDUwMjAwMDAwMFoXDTM4
# MDExODIzNTk1OVowfTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFu
# Y2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEYMBYGA1UEChMPU2VjdGlnbyBMaW1p
# dGVkMSUwIwYDVQQDExxTZWN0aWdvIFJTQSBUaW1lIFN0YW1waW5nIENBMIICIjAN
# BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAyBsBr9ksfoiZfQGYPyCQvZyAIVST
# uc+gPlPvs1rAdtYaBKXOR4O168TMSTTL80VlufmnZBYmCfvVMlJ5LsljwhObtoY/
# AQWSZm8hq9VxEHmH9EYqzcRaydvXXUlNclYP3MnjU5g6Kh78zlhJ07/zObu5pCNC
# rNAVw3+eolzXOPEWsnDTo8Tfs8VyrC4Kd/wNlFK3/B+VcyQ9ASi8Dw1Ps5EBjm6d
# J3VV0Rc7NCF7lwGUr3+Az9ERCleEyX9W4L1GnIK+lJ2/tCCwYH64TfUNP9vQ6oWM
# ilZx0S2UTMiMPNMUopy9Jv/TUyDHYGmbWApU9AXn/TGs+ciFF8e4KRmkKS9G493b
# kV+fPzY+DjBnK0a3Na+WvtpMYMyou58NFNQYxDCYdIIhz2JWtSFzEh79qsoIWId3
# pBXrGVX/0DlULSbuRRo6b83XhPDX8CjFT2SDAtT74t7xvAIo9G3aJ4oG0paH3uhr
# DvBbfel2aZMgHEqXLHcZK5OVmJyXnuuOwXhWxkQl3wYSmgYtnwNe/YOiU2fKsfqN
# oWTJiJJZy6hGwMnypv99V9sSdvqKQSTUG/xypRSi1K1DHKRJi0E5FAMeKfobpSKu
# pcNNgtCN2mu32/cYQFdz8HGj+0p9RTbB942C+rnJDVOAffq2OVgy728YUInXT50z
# vRq1naHelUF6p4MCAwEAAaOCAVowggFWMB8GA1UdIwQYMBaAFFN5v1qqK0rPVIDh
# 2JvAnfKyA2bLMB0GA1UdDgQWBBQaofhhGSAPw0F3RSiO0TVfBhIEVTAOBgNVHQ8B
# Af8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADATBgNVHSUEDDAKBggrBgEFBQcD
# CDARBgNVHSAECjAIMAYGBFUdIAAwUAYDVR0fBEkwRzBFoEOgQYY/aHR0cDovL2Ny
# bC51c2VydHJ1c3QuY29tL1VTRVJUcnVzdFJTQUNlcnRpZmljYXRpb25BdXRob3Jp
# dHkuY3JsMHYGCCsGAQUFBwEBBGowaDA/BggrBgEFBQcwAoYzaHR0cDovL2NydC51
# c2VydHJ1c3QuY29tL1VTRVJUcnVzdFJTQUFkZFRydXN0Q0EuY3J0MCUGCCsGAQUF
# BzABhhlodHRwOi8vb2NzcC51c2VydHJ1c3QuY29tMA0GCSqGSIb3DQEBDAUAA4IC
# AQBtVIGlM10W4bVTgZF13wN6MgstJYQRsrDbKn0qBfW8Oyf0WqC5SVmQKWxhy7VQ
# 2+J9+Z8A70DDrdPi5Fb5WEHP8ULlEH3/sHQfj8ZcCfkzXuqgHCZYXPO0EQ/V1cPi
# vNVYeL9IduFEZ22PsEMQD43k+ThivxMBxYWjTMXMslMwlaTW9JZWCLjNXH8Blr5y
# Umo7Qjd8Fng5k5OUm7Hcsm1BbWfNyW+QPX9FcsEbI9bCVYRm5LPFZgb289ZLXq2j
# K0KKIZL+qG9aJXBigXNjXqC72NzXStM9r4MGOBIdJIct5PwC1j53BLwENrXnd8uc
# Lo0jGLmjwkcd8F3WoXNXBWiap8k3ZR2+6rzYQoNDBaWLpgn/0aGUpk6qPQn1BWy3
# 0mRa2Coiwkud8TleTN5IPZs0lpoJX47997FSkc4/ifYcobWpdR9xv1tDXWU9UIFu
# q/DQ0/yysx+2mZYm9Dx5i1xkzM3uJ5rloMAMcofBbk1a0x7q8ETmMm8c6xdOlMN4
# ZSA7D0GqH+mhQZ3+sbigZSo04N6o+TzmwTC7wKBjLPxcFgCo0MR/6hGdHgbGpm0y
# XbQ4CStJB6r97DDa8acvz7f9+tCjhNknnvsBZne5VhDhIG7GrrH5trrINV0zdo7x
# fCAMKneutaIChrop7rRaALGMq+P5CslUXdS5anSevUiumDCCBvUwggTdoAMCAQIC
# EDlMJeF8oG0nqGXiO9kdItQwDQYJKoZIhvcNAQEMBQAwfTELMAkGA1UEBhMCR0Ix
# GzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEY
# MBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMSUwIwYDVQQDExxTZWN0aWdvIFJTQSBU
# aW1lIFN0YW1waW5nIENBMB4XDTIzMDUwMzAwMDAwMFoXDTM0MDgwMjIzNTk1OVow
# ajELMAkGA1UEBhMCR0IxEzARBgNVBAgTCk1hbmNoZXN0ZXIxGDAWBgNVBAoTD1Nl
# Y3RpZ28gTGltaXRlZDEsMCoGA1UEAwwjU2VjdGlnbyBSU0EgVGltZSBTdGFtcGlu
# ZyBTaWduZXIgIzQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCkkyhS
# S88nh3akKRyZOMDnDtTRHOxoywFk5IrNd7BxZYK8n/yLu7uVmPslEY5aiAlmERRY
# sroiW+b2MvFdLcB6og7g4FZk7aHlgSByIGRBbMfDCPrzfV3vIZrCftcsw7oRmB78
# 0yAIQrNfv3+IWDKrMLPYjHqWShkTXKz856vpHBYusLA4lUrPhVCrZwMlobs46Q9v
# qVqakSgTNbkf8z3hJMhrsZnoDe+7TeU9jFQDkdD8Lc9VMzh6CRwH0SLgY4anvv3S
# g3MSFJuaTAlGvTS84UtQe3LgW/0Zux88ahl7brstRCq+PEzMrIoEk8ZXhqBzNiuB
# l/obm36Ih9hSeYn+bnc317tQn/oYJU8T8l58qbEgWimro0KHd+D0TAJI3VilU6aj
# oO0ZlmUVKcXtMzAl5paDgZr2YGaQWAeAzUJ1rPu0kdDF3QFAaraoEO72jXq3nnWv
# 06VLGKEMn1ewXiVHkXTNdRLRnG/kXg2b7HUm7v7T9ZIvUoXo2kRRKqLMAMqHZkOj
# GwDvorWWnWKtJwvyG0rJw5RCN4gghKiHrsO6I3J7+FTv+GsnsIX1p0OF2Cs5dNta
# dwLRpPr1zZw9zB+uUdB7bNgdLRFCU3F0wuU1qi1SEtklz/DT0JFDEtcyfZhs43dB
# yP8fJFTvbq3GPlV78VyHOmTxYEsFT++5L+wJEwIDAQABo4IBgjCCAX4wHwYDVR0j
# BBgwFoAUGqH4YRkgD8NBd0UojtE1XwYSBFUwHQYDVR0OBBYEFAMPMciRKpO9Y/PR
# XU2kNA/SlQEYMA4GA1UdDwEB/wQEAwIGwDAMBgNVHRMBAf8EAjAAMBYGA1UdJQEB
# /wQMMAoGCCsGAQUFBwMIMEoGA1UdIARDMEEwNQYMKwYBBAGyMQECAQMIMCUwIwYI
# KwYBBQUHAgEWF2h0dHBzOi8vc2VjdGlnby5jb20vQ1BTMAgGBmeBDAEEAjBEBgNV
# HR8EPTA7MDmgN6A1hjNodHRwOi8vY3JsLnNlY3RpZ28uY29tL1NlY3RpZ29SU0FU
# aW1lU3RhbXBpbmdDQS5jcmwwdAYIKwYBBQUHAQEEaDBmMD8GCCsGAQUFBzAChjNo
# dHRwOi8vY3J0LnNlY3RpZ28uY29tL1NlY3RpZ29SU0FUaW1lU3RhbXBpbmdDQS5j
# cnQwIwYIKwYBBQUHMAGGF2h0dHA6Ly9vY3NwLnNlY3RpZ28uY29tMA0GCSqGSIb3
# DQEBDAUAA4ICAQBMm2VY+uB5z+8VwzJt3jOR63dY4uu9y0o8dd5+lG3DIscEld9l
# aWETDPYMnvWJIF7Bh8cDJMrHpfAm3/j4MWUN4OttUVemjIRSCEYcKsLe8tqKRfO+
# 9/YuxH7t+O1ov3pWSOlh5Zo5d7y+upFkiHX/XYUWNCfSKcv/7S3a/76TDOxtog3M
# w/FuvSGRGiMAUq2X1GJ4KoR5qNc9rCGPcMMkeTqX8Q2jo1tT2KsAulj7NYBPXyhx
# bBlewoNykK7gxtjymfvqtJJlfAd8NUQdrVgYa2L73mzECqls0yFGcNwvjXVMI8JB
# 0HqWO8NL3c2SJnR2XDegmiSeTl9O048P5RNPWURlS0Nkz0j4Z2e5Tb/MDbE6MNCh
# PUitemXk7N/gAfCzKko5rMGk+al9NdAyQKCxGSoYIbLIfQVxGksnNqrgmByDdefH
# fkuEQ81D+5CXdioSrEDBcFuZCkD6gG2UYXvIbrnIZ2ckXFCNASDeB/cB1PguEc2d
# g+X4yiUcRD0n5bCGRyoLG4R2fXtoT4239xO07aAt7nMP2RC6nZksfNd1H48QxJTm
# fiTllUqIjCfWhWYd+a5kdpHoSP7IVQrtKcMf3jimwBT7Mj34qYNiNsjDvgCHHKv6
# SkIciQPc9Vx8cNldeE7un14g5glqfCsIo0j1FfwET9/NIRx65fWOGtS5QDGCBlkw
# ggZVAgEBMGkwVDELMAkGA1UEBhMCR0IxGDAWBgNVBAoTD1NlY3RpZ28gTGltaXRl
# ZDErMCkGA1UEAxMiU2VjdGlnbyBQdWJsaWMgQ29kZSBTaWduaW5nIENBIFIzNgIR
# AJerP2s13dPOpevRLwsJhzgwCQYFKw4DAhoFAKB4MBgGCisGAQQBgjcCAQwxCjAI
# oAKAAKECgAAwGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIB
# CzEOMAwGCisGAQQBgjcCARUwIwYJKoZIhvcNAQkEMRYEFIvNpb+jaTvk2s7zWvY0
# 1hyjxq4ZMA0GCSqGSIb3DQEBAQUABIICAATCldrYfmgnpQbuDOh5e6hemYQIm7lq
# uGv5qZLENFPvmh5EInfmNU195GKwgcORO8dtLqKz+SuM8zF+Hk1VOYmOBX/5+SWL
# oyvUDw5mGwNDp16jNX62tG0C0L76KbxfZShigzapwWnvsqhhsOhW5GFGCbrIW9NI
# Tyfejp8K6X5iK44HPmOIsaHvBDu9KF9wjwChy2l9mI2TebhY+ZFFHFy625XkMlH7
# tjOZwPKDeksWEm2mB46r3X2wixZiKXDT2+FqFnka0pblNOwLmSBdU1+DIdg6T0Hd
# GQ9HxRHTRAP1j2AB2zdfUZNOYHBEKE9Cu8WGhnD1SymFv6XI94djAWse1xo6iPTe
# T0j3+Eo9C8zigCKkybQeeaVMihA8fL9iWeyLv5ylZsliipe5+41ZoQh+F83nJifI
# RUl3UMNUfdSfmgyLN/TroxnWxFCB84pZKuvc05pgHcRDASFmAajGETVyHsrcFev4
# g3A9q5/h79L0BHkqlctmWrUpZuC7PXlrLrss9u406kv18H40wBmNOqNQ8PohxIFR
# Jlp24vwlFEQ+paV3s2uynE2J89cw/DTkvcsFvWo3eb8roCVIuJXLhqALbBX9tmXV
# pRdcQJekNdz7WpCdCl0jWcuYCkaA5LNnhsmcKd9r5MP8LG/cLDi+6vnOS0Vjg2GK
# SsHkr86xyMtfoYIDSzCCA0cGCSqGSIb3DQEJBjGCAzgwggM0AgEBMIGRMH0xCzAJ
# BgNVBAYTAkdCMRswGQYDVQQIExJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAOBgNVBAcT
# B1NhbGZvcmQxGDAWBgNVBAoTD1NlY3RpZ28gTGltaXRlZDElMCMGA1UEAxMcU2Vj
# dGlnbyBSU0EgVGltZSBTdGFtcGluZyBDQQIQOUwl4XygbSeoZeI72R0i1DANBglg
# hkgBZQMEAgIFAKB5MBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcN
# AQkFMQ8XDTIzMDcwNTE0MjM0OFowPwYJKoZIhvcNAQkEMTIEMCTrWkjEGzLRLrSl
# l6fnWiMV0iZIrej5fggoWrltO+WwR1zp0U83CZBq51PZmf0HqTANBgkqhkiG9w0B
# AQEFAASCAgBMW/IhXU8XXMPIB0RYBT1bjkh9Ha47nAGzhLW0WWUhnOu97dq5fiJ6
# VKWGWwAyWc1ay4/y/nWMnZ7PGjmxt7PPZI4tPaRby32k9jkMRPsO0QoCeSs2wQmu
# 9REhJIiAXIH5Mx4IgWWOD8SsA+zvbHHiutmo625uQnSBbfIW1+kF+1mmIxr7kuvk
# psPKfZ50R9OA6I3mpbXFXTriz8E7YyJ9Q0gI3pzKbc8RN2y6rDAC6AVPcbx8xLjC
# e5Pa2c7TvtvOzjt6Sh/w5CJxQbEOaUi9TnDnBTbFcQksmm2LEh6aSorVuV+/nH/l
# wmZI6yDTePAnt2fUeY4ybfH1nmWmM7tsQ0guiiG/l5mCp6cNLdMGD4MuUw/jlKnS
# Skdzpxb/+/xpHRC/BPX2JQmVSGcs2m/goZKiAeVZnP5Nq/vaPXpN75p7h/WlT9KK
# TJp6rCOZKXzKb5qWEUwoQ/YP69HO0ViL8SOk8UGuQIyOK5HGyMfoDTw9xB2/ndpn
# 7FAEfqNw4/+u8J5D1lnI4tWW50Wms1/h/yPODTA0AvVA45qQo8PWmc+vlTY3Wz6q
# nIXAPDCdYkEe9g0DauE6pcugBENhniQDthOth83DbYZT6Fv+szPgbelaEW+GyaKR
# Jwi263Ry8xmY3oZRY445ptj/fPlCAV1q2g3pkCz8WQlO4gHVMed0+A==
# SIG # End signature block
