Pulse Secure and the bells around it

So you have implemented Pulse Secure as a VPN-solution? Well, here are somethings that just about every consulting firm or department eventually will ask you to implement along-side it – so the Pulse Secure client works. The details aren’t here – but more the general mindset, what todo when – but not exactly how every scheduled task / script looks like. Hopefully this will guide you – and potentially arm you with possibility to fix more problems in the future. 

Triggers

First things first – a great way to trigger activity is based on Pulse Secure events – described more in detailed on their admin-guide. Common event-ids that might be useful are;

Disconnect – 302 / 106

Connect – 312 (not documented)

To create a scheduled task for any event – simply find a sample event a select to create a Scheduled Task;

image

In addition to Application and Services log / Pulse Secure, you can always find Microsoft-Windows-NetworkProfile/Operational. This contains great entries for what network you connect to, what network profile type of profile you connect to. Event-id 1000 within NetworkProfile is simply that you are connected to a new network as a sample.

Actions

What actions will be asked to run when these events happen?

Re-register DNS

Name-resolution for the helpdesk not quick-enough to connect to your laptop once you are connected? On event Connect (detected by Pulse Secure/Operational and the event id 312) you can simply have the scheduled task run the command-line – and this will mitigate some stuff. 

ipconfig /registerdns

DNS Suffix search list

Pulse normally appends the searchlist of the DNS-suffixes that are set on your client. It also fails to clean this up properly – so name resolution after a disconnect can be challenging. A tidy way todo this would be to trigger on a disconnect (primarily event id 302 within Pulse Secure, but also 106 might be applicable) and then do a sweep. Sample code;

$dnsCGSetting = Get-DnsClientGlobalSetting
$SList = $dnsCGSetting.SuffixSearchList
If ([string]::IsNullOrEmpty($SList) -or $SList -eq "OK.suffix")
{
#donothing
}
Else{
Set-DnsClientGlobalSetting -SuffixSearchList @('')
}

SuperUser has some options if you have a longer list of suffixes to handle..

Host-checker antivirus check

Did someone implement a host-checker and decided something needs to be up-2-date? Like antivirus definitions? Use trigger NetworkProfile with EventID to identify that a device has successfully connected to a network, and then do a check which network – and if not the corporate one – start up the processes to ensure users can avoid having a failed connect.

Sample functions (to check what network and do a validation of Defender AV Signature) – in VBscript as this was to be firing of quite heavily on all endpoints.

Private Function NetConnectionProfileName(Network)
	Dim objWMIService
	Dim colItems

	NetConnectionProfileName = False
	
	On Error Resume Next
	Set objWMIService = GetObject("winmgmts:\\.\root\StandardCimv2")

	Set colItems = objWMIService.ExecQuery("Select * From MSFT_NetConnectionProfile")

	For Each objItem in colItems
		if objItem.Name = Network Then 
			NetConnectionProfileName= True
		End if 
	Next

	Set objWMIService = Nothing

End Function

Private Function DefenderSignatureUpdate
	Dim objWMIService
	Dim colItems
	
	DefenderSignatureUpdate = False

	On Error Resume Next
	Set objWMIService = GetObject("winmgmts:\\.\root\Microsoft\Windows\Defender")

	Set colItems = objWMIService.ExecQuery("Select * From MSFT_MPComputerStatus")
	Wscript.echo "Definition-update: " & Left(objItem.AntivirusSignatureLastUpdated,8) & "Today: " & year(now())&right("00" & month(now),2)&right("00" & Day(now),2)
	For Each objItem in colItems
		if Left(objItem.AntivirusSignatureLastUpdated,8) = year(now())&right("00" & month(now),2)&right("00" & Day(now),2) Then 
			
			DefenderSignatureUpdate = True
		End if 
	Next



	Set objWMIService = Nothing

End Function

A similiar way within Powershell to detect a Domain-profile is posted on Twitter:

Windows Firewall profile doesn’t always switch to Domain when you use a third-party VPN client

This isn’t really an action. You could most likely trigger something off the event ids – however, Microsoft has documented a great article about this problem. Pulse allegedly fixed this in a really old-version of the client, but to this day noone is happy about it. Set the two workarounds as part of your baseline for devices connecting via VPN – and you should be good. Pulse (now owned by Ivanti) will not be fixing this it seems. As far as I can tell Pulse adds routes, and then doesn’t notify Windows in anyway that triggers a rediscovery for Domain-connections.

Microsoft states:

  • First, disable Domain Discovery negative cache by adding the NegativeCachePeriod registry key to following subkey: HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\NetLogon\Parameters Name: NegativeCachePeriod
    Type: REG_DWORD
    Value Data: 0 (default value: 45 seconds; set to 0 to disable caching)
  • If issue doesn’t resolve, further disable DNS negative cache by adding the MaxNegativeCacheTtl registry key to the following subkey: HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Dnscache\Parameters Name: MaxNegativeCacheTtl
    Type: REG_DWORD
    Value Data: 0 (default value: 5 seconds; set to 0 to disable caching)

Force a new network-profile

Your client still skipping around and not getting stuff right? Well, you can switch over to a Private Profile. Public is by default, and Domain requires that a Active Directory is reachable (process documented in previous article from Microsoft) – but Private you can switch to.

See this one-liner to detect a specific network and switch the Network Profile

Get-NetConnectionProfile -Name 'networkname' | Set-NetConnectionProfile -NetworkCategory Private

NTUser.dat and last updated

Regardless what type of estate of Windows-devices, there always seems to be a need of clearing out unused profiles from a computer to save diskspace, increase performance and what not.

In Windows 7 there was an issue (resolved by a hotfix) that simply loading up a ntuser.dat file would change the timestamp of when it was last written to. It seems that this has now been the defacto default behaviour for Windows 10, and a long-running thread disusses different ways of adressing the issue – how can you identify if a profile was recently used on a device? Nirsoft tools (aren’t they great?) provide a great and easy to read overview if logon history based on security event logs. 

That seems tedious. Using the written time for the folder doesn’t seem to be accurate – and the risk of removing active user profiles is high. However, if one could track the last-write time for the registry entry for the profile – we should be good, right? Unfortunately – last write time for the registry entry isn’t there out of the box using Powershell (or VBScript etc). Seems to be a few things posted on Technet Gallery (to be gone soon) that will provide the missing piecies. 

Where are we looking? Right here;

image

Use the function Add-RegKeyMember, loop through all profiles and then filter any potential things you want to leave behind – and we should be able to clear out not so active profiles. A few dangerous lines commented out so you can copy and paste at will. 

function Add-RegKeyMember {
 <#
 .SYNOPSIS
Adds note properties containing the last modified time and class name of a 
registry key.

.DESCRIPTION
 The Add-RegKeyMember function uses the unmanged RegQueryInfoKey Win32 function
 to get a key's last modified time and class name. It can take a RegistryKey 
object (which Get-Item and Get-ChildItem output) or a path to a registry key.

.EXAMPLE
 PS> Get-Item HKLM:\SOFTWARE | Add-RegKeyMember | Select Name, LastWriteTime

Show the name and last write time of HKLM:\SOFTWARE

.EXAMPLE
 PS> Add-RegKeyMember HKLM:\SOFTWARE | Select Name, LastWriteTime

Show the name and last write time of HKLM:\SOFTWARE

.EXAMPLE
 PS> Get-ChildItem HKLM:\SOFTWARE | Add-RegKeyMember | Select Name, LastWriteTime

Show the name and last write time of HKLM:\SOFTWARE's child keys

.EXAMPLE
 PS> Get-ChildItem HKLM:\SYSTEM\CurrentControlSet\Control\Lsa | Add-RegKeyMember | where classname | select name, classname

Show the name and class name of child keys under Lsa that have a class name defined.

.EXAMPLE
 PS> Get-ChildItem HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall | Add-RegKeyMember | where lastwritetime -gt (Get-Date).AddDays(-30) | 
 >> select PSChildName, @{ N="DisplayName"; E={gp $_.PSPath | select -exp DisplayName }}, @{ N="Version"; E={gp $_.PSPath | select -exp DisplayVersion }}, lastwritetime |
 >> sort lastwritetime

Show applications that have had their registry key updated in the last 30 days (sorted by the last time the key was updated).
 NOTE: On a 64-bit machine, you will get different results depending on whether or not the command was executed from a 32-bit
       or 64-bit PowerShell prompt.

#>

    [CmdletBinding()]
     param(
         [Parameter(Mandatory, ParameterSetName="ByKey", Position=0, ValueFromPipeline)]
         # Registry key object returned from Get-ChildItem or Get-Item
         [Microsoft.Win32.RegistryKey] $RegistryKey,
         [Parameter(Mandatory, ParameterSetName="ByPath", Position=0)]
         # Path to a registry key
         [string] $Path
     )

    begin {
         # Define the namespace (string array creates nested namespace):
         $Namespace = "CustomNamespace", "SubNamespace"

        # Make sure type is loaded (this will only get loaded on first run):
         Add-Type @"
             using System; 
             using System.Text;
             using System.Runtime.InteropServices; 

            $($Namespace | ForEach-Object {
                 "namespace $_ {"
             })

                public class advapi32 {
                     [DllImport("advapi32.dll", CharSet = CharSet.Auto)]
                     public static extern Int32 RegQueryInfoKey(
                         Microsoft.Win32.SafeHandles.SafeRegistryHandle hKey,
                         StringBuilder lpClass,
                         [In, Out] ref UInt32 lpcbClass,
                         UInt32 lpReserved,
                         out UInt32 lpcSubKeys,
                         out UInt32 lpcbMaxSubKeyLen,
                         out UInt32 lpcbMaxClassLen,
                         out UInt32 lpcValues,
                         out UInt32 lpcbMaxValueNameLen,
                         out UInt32 lpcbMaxValueLen,
                         out UInt32 lpcbSecurityDescriptor,
                         out Int64 lpftLastWriteTime
                     );
                 }
             $($Namespace | ForEach-Object { "}" })
 "@
     
         # Get a shortcut to the type:    
         $RegTools = ("{0}.advapi32" -f ($Namespace -join ".")) -as [type]
     }

    process {
         switch ($PSCmdlet.ParameterSetName) {
             "ByKey" {
                 # Already have the key, no more work to be done :)
             }

            "ByPath" {
                 # We need a RegistryKey object (Get-Item should return that)
                 $Item = Get-Item -Path $Path -ErrorAction Stop

                # Make sure this is of type [Microsoft.Win32.RegistryKey]
                 if ($Item -isnot [Microsoft.Win32.RegistryKey]) {
                     throw "'$Path' is not a path to a registry key!"
                 }
                 $RegistryKey = $Item
             }
         }

        # Initialize variables that will be populated:
         $ClassLength = 255 # Buffer size (class name is rarely used, and when it is, I've never seen 
                             # it more than 8 characters. Buffer can be increased here, though. 
         $ClassName = New-Object System.Text.StringBuilder $ClassLength  # Will hold the class name
         $LastWriteTime = $null
             
         switch ($RegTools::RegQueryInfoKey($RegistryKey.Handle,
                                     $ClassName, 
                                     [ref] $ClassLength, 
                                     $null,  # Reserved
                                     [ref] $null, # SubKeyCount
                                     [ref] $null, # MaxSubKeyNameLength
                                     [ref] $null, # MaxClassLength
                                     [ref] $null, # ValueCount
                                     [ref] $null, # MaxValueNameLength 
                                     [ref] $null, # MaxValueValueLength 
                                     [ref] $null, # SecurityDescriptorSize
                                     [ref] $LastWriteTime
                                     )) {

            0 { # Success
                 $LastWriteTime = [datetime]::FromFileTime($LastWriteTime)

                # Add properties to object and output them to pipeline
                 $RegistryKey | Add-Member -NotePropertyMembers @{
                     LastWriteTime = $LastWriteTime
                     ClassName = $ClassName.ToString()
                 } -PassThru -Force
             }

            122  { # ERROR_INSUFFICIENT_BUFFER (0x7a)
                 throw "Class name buffer too small"
                 # function could be recalled with a larger buffer, but for
                 # now, just exit
             }

            default {
                 throw "Unknown error encountered (error code $_)"
             }
         }
     }
 }


             $profiles = Get-ChildItem "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList" | Add-RegKeyMember | Select Name, LastWriteTime
             foreach ($p in $profiles) {
             
             if ($p.lastwritetime -lt $(Get-Date).Date.AddDays(-90)) {
                     
                     $key = $($p.name) -replace "HKEY_LOCAL_MACHINE","HKLM:"
                     $path = (Get-ItemProperty  -Path $key -Name ProfileImagePath).ProfileImagePath
                     $path.tolower()
                     if ($path.ToLower() -notlike 'c:\windows\*' -and $path.ToLower() -notlike 'c:\users\adm*') {
                         write-host "delete " $path
                         #Get-CimInstance -Class Win32_UserProfile | Where-Object { $path.split('\')[-1] -eq $User } | Remove-CimInstance #-ErrorAction SilentlyContinue
                         #Add-Content c:\windows\temp\DeleteProfiles.log -Value "$User was deleted from this computer."
                     }

                }
             }

Lenovo and BIOS Settings

Lenovo has provided numerous series of devices, which apparently do not have an aligned standard of handling BIOS / UEFI settings or BIOS / UEFI upgrades. As part of this headache and to ease the burden of jumping between the different device types I started to dive in to harmonize our scripts. As previously written Lenovo has a few odd design decisions – you can for example not reduce security via script. To disable TPM or SecureBoot you would be required to alter this via a keyboard. However, setting the initial password (increasing the security) also requires that you do this by manually typing it into the settings. In addition to the above challenges I reached the following conclusion;

1. It is most likely only relevant to enable any setting.
2. Handling passwords stinks (setting them etc)
3.  Settings between different models in a series can vary by name and vary by name of the possible values to set
4. WMI-classes used differs between series (ThinkStation, ThinkPad and ThinkCentre)

For example, ThinkPad has the GetBIOSSelections class, which can present the different possibilities for a specific setting. However, ThinkCentre does not have this class, but instead present options via a [bracket] indicator in the output of the currentsetting.

How would one deal with all this mess?

Two functions are provided – one for figuring our howto enable a specific setting. The other one for setting each setting. Its ugly – as the more models, bios-versions and whatnot I get a hold of the uglier variations of this I find. The initial attempt was to keep it as generic as possible.The best way to explain what I have todo sometimes;

 

Function Get-LenovoBIOSSettingValue {
    param(
        [Alias("BIOSSetting")]
        [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Array]$Setting
        
    ) 

    begin {
            $complex = $null
            if(Get-WmiObject -namespace root\wmi -List | where { $_.Name -eq "Lenovo_GetBiosSelections"})
            {
                      $complex = $false
                      Write-verbose "Lenovo GetBiosSelections does exist"
            }
            else
            {
                      $complex = $true
                      Write-verbose "Lenovo GetBiosSelections does not exist"
            }
    }

    process {
            foreach ($s in $setting) {
                $value = $null
                $selections = $null
                $findsetting = $null
        
                If ($complex -eq $true) {
                    $findsetting = gwmi -class Lenovo_BiosSetting -namespace root\wmi | Where-Object {$_.CurrentSetting.split(",",[StringSplitOptions]::RemoveEmptyEntries) -like $s} | Select-Object CurrentSetting
                    try {
                        $value = $findsetting.CurrentSetting.split(",",[StringSplitOptions]::RemoveEmptyEntries)[0]
                    }
                    catch {
                       # write-output "Unable to convert $s to value"
                    }                        
                    $r = [regex] '(?<=\[).+?(?=\])'
                    try {

                         [string]$checkvalue = $findsetting.CurrentSetting.split(",",[StringSplitOptions]::RemoveEmptyEntries)[1].split(";",[StringSplitOptions]::RemoveEmptyEntries)[0]
                         $appliedselection = $checkvalue
                        if ([string]$($findsetting.CurrentSetting.split(";",[StringSplitOptions]::RemoveEmptyEntries)[1]) -Match 'Status:ShowOnly') {


                            if($checkvalue -like  "Active*" -or $checkvalue -like  "Enabl*"  -or $checkvalue -like 'Discrete*TPM*' ) 
                            {
                                    if ($value -like 'Security*Chip*') {
                                            $value = $null
                                            $selections = $null
                                            $findsetting = $null
                                    }
                                
                            }
                            else {
                                $value = $null
                                $selections = $null
                                $findsetting = $null
                            }
                        }
                    }
                    catch {
                        #noerror
                    }
                    $selections = ($r.match($findsetting.currentsetting).value).split(":").split(",") | Where-Object {$_ -like "Active*" -or $_ -like "Enabl*" -or $_ -like 'Discrete*TPM*' }
                    
                    if ($selections -is [array] -and $selections -contains 'Enable') {
                        $selections = $selections| Where-Object { $_ -like "Enabl*" }
                    }
    
                }
                else {

                    
                    try {
                        $findsetting = Get-WmiObject -Class Lenovo_BiosSetting -Namespace root\WMI | Where-Object {$_.CurrentSetting -match $s} | Select-Object CurrentSetting
                        $value = $findsetting.currentsetting.split(",")[0]
                        $appliedselection = $findsetting.currentsetting.split(",")[1]
                    }
                    catch {
                        #write-output "Unable to convert $s to value"
                    }

                    $selections = ((gwmi –class Lenovo_GetBiosSelections –namespace root\wmi).GetBiosSelections($value).selections).split(",") | Where-Object {$_ -like "Active*" -or $_ -like "Enabl*" }
                    if ($selections -is [array] -and $selections -contains 'Enable') {
                        $selections = $selections| Where-Object { $_ -like "Enabl*" }
                    }
                }
                if ($value -ne $null -and $selections -ne $null) {
                    $object = New-Object –TypeName PSObject
                    $object | Add-Member –MemberType NoteProperty –Name Setting –Value $($value)
                    $object | Add-Member –MemberType NoteProperty –Name Current –Value $($appliedselection)
                    $object | Add-Member –MemberType NoteProperty –Name Selection –Value $($selections) 
                    $object

                    #write-output "$s provided $value, which can be set to $selections"
                }
            }
    }
}

Function Set-LenovoBIOSSetting {
    param(
        [Alias("BIOSSetting")]
        [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Array]$Setting,
        [Parameter(Position = 1, Mandatory = $false)]
        [String]$Password
        
    )

    begin {
        $wmi = Get-WmiObject -Class Lenovo_SetBiosSetting -Namespace root\wmi -ErrorAction Stop
        $object = $null
        if ($object) {
            Remove-Variable -Name $object
        }
        #$setting
    }

    process {
    
     
        foreach ($s in $setting) {
        #$s
            $result = $null
                 if ($s.Current -ne $s.Selection) {
                       $result = ($wmi.SetBiosSetting("$($s.setting),$($s.selection)$password")).return
                        $object = New-Object –TypeName PSObject
                        $object | Add-Member –MemberType NoteProperty –Name Setting –Value $($s.setting)
                        $object | Add-Member –MemberType NoteProperty –Name Current –Value $($s.selection)
                        $object | Add-Member –MemberType NoteProperty –Name Result –Value $result
                        $object
                 }
                 else {

                        $object = New-Object –TypeName PSObject
                        $object | Add-Member –MemberType NoteProperty –Name Setting –Value $($s.setting)
                        $object | Add-Member –MemberType NoteProperty –Name Current –Value $($s.Current)
                        $object | Add-Member –MemberType NoteProperty –Name Result –Value 'Not updated'
                        $object
                }
         }
    }

}

Get-FileInfo

Had to retrieve information if a filed was locked and who owned it – so wrote the below Powershell function. Ways to improve would perhaps be to provide parsing of multiple-files.

<?Function Get-FileInfo {
<#
.SYNOPSIS
Retrieves file-information, such as size, name and locks
.DESCRIPTION
Outputs an object with Path, Size, Created on, Last Write Time,
Owner and if the file is locked
.EXAMPLE
Get-FileInfo -File c:\windows\regedit.exe
#>
[CmdletBinding()]
param(
[Parameter(mandatory=$true)]
[string]$File
)
Begin
{
write-verbose "------------------------"
write-verbose "Start of Get-FileInfo"
write-verbose "Computername:  $($env:computername)"
write-verbose "Username: $($env:USERNAME)"
Write-verbose "Validate file $($file)"

if (Test-Path  $($file))
{
Write-Verbose "File exists"
}
else
{
throw-error "File does not exist"
}

}
Process
{

#Retrieve file object
$objfile = Get-ChildItem $file

#check file lock
try { [IO.File]::OpenWrite($objfile).close();$lock = $false }
catch {$lock = $true}

#output object
New-Object PSObject -Property @{
Path = $objFile.fullname
Size = "{0:N2} MB" -f ( $objFile.Length / 1mb )
'Created on' = $objFile.CreationTime
'Last Write Time' = $objFile.LastWriteTime
Owner = (Get-Acl $File).Owner
Lock = $lock

}

}
End
{
write-verbose "End of Get-FileInfo"
write-verbose "------------------------"
}
}

AppsNotify 3.0

As a follow-up to a previous post – here comes a revised version of AppsNotify.

Changes

  • Possible to exclude applications that notify users
    Create a registry value under HKLM\Software\AppsNotify\AppsNotifyExclusion that matches the application that needs to be excluded
  • Detect if the C: – drive is low on disk and exit before attempt to write anything to disk
  • Notifications now include name of applications that are new, up to 5 new applications
  • Detect if the computer is idle and do not check AppCatalog and do not notify user
    -Avoids hammering of the website and notifications going nowhere
  • Log-file is generated in users temp folder: \appsnotify app.log

Code from elsewhere

Idle-time from StackOverflow
Most functions (not Logs – as previously stated in old blog-post) are from PowerShell Studio
Or written by me. See previous post.

Parameter

Pass on the parameter appcatalog with the url for Application Catalog – sample;

-appcatalog http://appcatalog.yourcompany.com:81/cmapplicationCatalog

Run it

Normally I wrap this within an executable that file (can run with Powershell 2/3 and should run as logged on user). How often? Well – sample code from an exported Task Scheduler

<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.2" xmlns="<a href="http://schemas.microsoft.com/windows/2004/02/mit/task&quot;">http://schemas.microsoft.com/windows/2004/02/mit/task"</a>>
<RegistrationInfo>
<Author></Author>
</RegistrationInfo>
<Triggers>
<LogonTrigger>
<Repetition>
<Interval>PT10M</Interval>
<StopAtDurationEnd>false</StopAtDurationEnd>
</Repetition>
<StartBoundary>1899-12-30T06:04:14</StartBoundary>
<Enabled>true</Enabled>
<Delay>PT15M</Delay>
</LogonTrigger>
</Triggers>
<Principals>
<Principal id="Author">
<GroupId>S-1-5-32-545</GroupId>
<RunLevel>LeastPrivilege</RunLevel>
</Principal>
</Principals>
<Settings>
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
<AllowHardTerminate>true</AllowHardTerminate>
<StartWhenAvailable>true</StartWhenAvailable>
<RunOnlyIfNetworkAvailable>true</RunOnlyIfNetworkAvailable>
<IdleSettings>
<StopOnIdleEnd>true</StopOnIdleEnd>
<RestartOnIdle>false</RestartOnIdle>
</IdleSettings>
<AllowStartOnDemand>true</AllowStartOnDemand>
<Enabled>true</Enabled>
<Hidden>true</Hidden>
<RunOnlyIfIdle>false</RunOnlyIfIdle>
<WakeToRun>false</WakeToRun>
<ExecutionTimeLimit>PT4H</ExecutionTimeLimit>
<Priority>7</Priority>
</Settings>
<Actions Context="Author">
<Exec>
<Command>"C:\Program Files (x86)\Common Files\AppsNotify\AppsNotify.exe"</Command>
<Arguments>-appcatalog http://website:8080/cmapplicationCatalog</Arguments>
</Exec>
</Actions>
</Task>

Code

Or download it

#========================================================================
# Created on:    2017-08-16
# Created by:    Nicke Källén
# Organization:
# Filename:        AppsNotify 3.0.pff
#========================================================================
Add-Type @'
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;

namespace PInvoke.Win32 {

public static class UserInput {

[DllImport("user32.dll", SetLastError=false)]
private static extern bool GetLastInputInfo(ref LASTINPUTINFO plii);

[StructLayout(LayoutKind.Sequential)]
private struct LASTINPUTINFO {
public uint cbSize;
public int dwTime;
}

public static DateTime LastInput {
get {
DateTime bootTime = DateTime.UtcNow.AddMilliseconds(-Environment.TickCount);
DateTime lastInput = bootTime.AddMilliseconds(LastInputTicks);
return lastInput;
}
}

public static TimeSpan IdleTime {
get {
return DateTime.UtcNow.Subtract(LastInput);
}
}

public static int LastInputTicks {
get {
LASTINPUTINFO lii = new LASTINPUTINFO();
lii.cbSize = (uint)Marshal.SizeOf(typeof(LASTINPUTINFO));
GetLastInputInfo(ref lii);
return lii.dwTime;
}
}
}
}
'@

$AppNotify.FormBorderStyle = 'FixedToolWindow'
Function Log-Start{
<#
.SYNOPSIS
Creates log file

.DESCRIPTION
Creates log file with path and name that is passed. Checks if log file exists, and if it does deletes it and creates a new one.
Once created, writes initial logging data

.PARAMETER LogPath
Mandatory. Path of where log is to be created. Example: C:\Windows\Temp

.PARAMETER LogName
Mandatory. Name of log file to be created. Example: Test_Script.log

.PARAMETER ScriptVersion
Mandatory. Version of the running script which will be written in the log. Example: 1.5

.INPUTS
Parameters above

.OUTPUTS
Log file created

.NOTES
Version:        1.0
Author:         Luca Sturlese
Creation Date:  10/05/12
Purpose/Change: Initial function development

Version:        1.1
Author:         Luca Sturlese
Creation Date:  19/05/12
Purpose/Change: Added debug mode support

.EXAMPLE
Log-Start -LogPath "C:\Windows\Temp" -LogName "Test_Script.log" -ScriptVersion "1.5"
#>

[CmdletBinding()]

Param ([Parameter(Mandatory=$true)][string]$LogPath, [Parameter(Mandatory=$true)][string]$LogName, [Parameter(Mandatory=$true)][string]$ScriptVersion)

Process{
$sFullPath = $LogPath + "\" + $LogName

#Check if file exists and delete if it does
If((Test-Path -Path $sFullPath)){
Remove-Item -Path $sFullPath -Force
}

#Create file and start logging
New-Item -Path $LogPath -Name $LogName –ItemType File

Add-Content -Path $sFullPath -Value "***************************************************************************************************"
Add-Content -Path $sFullPath -Value "Started processing at [$([DateTime]::Now)]."
Add-Content -Path $sFullPath -Value "***************************************************************************************************"
Add-Content -Path $sFullPath -Value ""
Add-Content -Path $sFullPath -Value "Running script version [$ScriptVersion]."
Add-Content -Path $sFullPath -Value ""
Add-Content -Path $sFullPath -Value "***************************************************************************************************"
Add-Content -Path $sFullPath -Value ""

#Write to screen for debug mode
Write-Debug "***************************************************************************************************"
Write-Debug "Started processing at [$([DateTime]::Now)]."
Write-Debug "***************************************************************************************************"
Write-Debug ""
Write-Debug "Running script version [$ScriptVersion]."
Write-Debug ""
Write-Debug "***************************************************************************************************"
Write-Debug ""

}
}

Function Log-Write{
<#
.SYNOPSIS
Writes to a log file

.DESCRIPTION
Appends a new line to the end of the specified log file

.PARAMETER LogPath
Mandatory. Full path of the log file you want to write to. Example: C:\Windows\Temp\Test_Script.log

.PARAMETER LineValue
Mandatory. The string that you want to write to the log

.INPUTS
Parameters above

.OUTPUTS
None

.NOTES
Version:        1.0
Author:         Luca Sturlese
Creation Date:  10/05/12
Purpose/Change: Initial function development

Version:        1.1
Author:         Luca Sturlese
Creation Date:  19/05/12
Purpose/Change: Added debug mode support

.EXAMPLE
Log-Write -LogPath "C:\Windows\Temp\Test_Script.log" -LineValue "This is a new line which I am appending to the end of the log file."
#>

[CmdletBinding()]

Param ([Parameter(Mandatory=$true)][string]$LogPath, [Parameter(Mandatory=$true)][string]$LineValue)

Process{
Add-Content -Path $LogPath -Value $LineValue

#Write to screen for debug mode
Write-Debug $LineValue
}
}

Function Log-Error{
<#
.SYNOPSIS
Writes an error to a log file

.DESCRIPTION
Writes the passed error to a new line at the end of the specified log file

.PARAMETER LogPath
Mandatory. Full path of the log file you want to write to. Example: C:\Windows\Temp\Test_Script.log

.PARAMETER ErrorDesc
Mandatory. The description of the error you want to pass (use $_.Exception)

.PARAMETER ExitGracefully
Mandatory. Boolean. If set to True, runs Log-Finish and then exits script

.INPUTS
Parameters above

.OUTPUTS
None

.NOTES
Version:        1.0
Author:         Luca Sturlese
Creation Date:  10/05/12
Purpose/Change: Initial function development

Version:        1.1
Author:         Luca Sturlese
Creation Date:  19/05/12
Purpose/Change: Added debug mode support. Added -ExitGracefully parameter functionality

.EXAMPLE
Log-Error -LogPath "C:\Windows\Temp\Test_Script.log" -ErrorDesc $_.Exception -ExitGracefully $True
#>

[CmdletBinding()]

Param ([Parameter(Mandatory=$true)][string]$LogPath, [Parameter(Mandatory=$true)][string]$ErrorDesc, [Parameter(Mandatory=$true)][boolean]$ExitGracefully)

Process{
Add-Content -Path $LogPath -Value "Error: An error has occurred [$ErrorDesc]."

#Write to screen for debug mode
Write-Debug "Error: An error has occurred [$ErrorDesc]."

#If $ExitGracefully = True then run Log-Finish and exit script
If ($ExitGracefully -eq $True){
Log-Finish -LogPath $LogPath
Break
}
}
}

Function Log-Finish{
<#
.SYNOPSIS
Write closing logging data & exit

.DESCRIPTION
Writes finishing logging data to specified log and then exits the calling script

.PARAMETER LogPath
Mandatory. Full path of the log file you want to write finishing data to. Example: C:\Windows\Temp\Test_Script.log

.PARAMETER NoExit
Optional. If this is set to True, then the function will not exit the calling script, so that further execution can occur

.INPUTS
Parameters above

.OUTPUTS
None

.NOTES
Version:        1.0
Author:         Luca Sturlese
Creation Date:  10/05/12
Purpose/Change: Initial function development

Version:        1.1
Author:         Luca Sturlese
Creation Date:  19/05/12
Purpose/Change: Added debug mode support

Version:        1.2
Author:         Luca Sturlese
Creation Date:  01/08/12
Purpose/Change: Added option to not exit calling script if required (via optional parameter)

.EXAMPLE
Log-Finish -LogPath "C:\Windows\Temp\Test_Script.log"

.EXAMPLE
Log-Finish -LogPath "C:\Windows\Temp\Test_Script.log" -NoExit $True
#>

[CmdletBinding()]

Param ([Parameter(Mandatory=$true)][string]$LogPath, [Parameter(Mandatory=$false)][string]$NoExit)

Process{
Add-Content -Path $LogPath -Value ""
Add-Content -Path $LogPath -Value "***************************************************************************************************"
Add-Content -Path $LogPath -Value "Finished processing at [$([DateTime]::Now)]."
Add-Content -Path $LogPath -Value "***************************************************************************************************"

#Write to screen for debug mode
Write-Debug ""
Write-Debug "***************************************************************************************************"
Write-Debug "Finished processing at [$([DateTime]::Now)]."
Write-Debug "***************************************************************************************************"

#Exit calling script if NoExit has not been specified or is set to False
If(!($NoExit) -or ($NoExit -eq $False)){
Exit
}

}
}
function Get-ScriptDirectory
{
if($hostinvocation -ne $null)
{
Split-Path $hostinvocation.MyCommand.path
}
else
{
Split-Path $script:MyInvocation.MyCommand.Path
}
}

function Parse-Commandline
{
<#
.SYNOPSIS
Parses the Commandline of a package executable

.DESCRIPTION
Parses the Commandline of a package executable

.PARAMETER  Commandline
The Commandline of the package executable

.EXAMPLE
$arguments = Parse-Commandline -Commandline $Commandline

.INPUTS
System.String

.OUTPUTS
System.Collections.Specialized.StringCollection
#>

[OutputType([System.Collections.Specialized.StringCollection])]
Param([string]$CommandLine)

$Arguments = New-Object System.Collections.Specialized.StringCollection

if($CommandLine)
{
#Find First Quote
$index = $CommandLine.IndexOf('"')

while ( $index -ne -1)
{#Continue as along as we find a quote
#Find Closing Quote
$closeIndex = $CommandLine.IndexOf('"',$index + 1)
if($closeIndex -eq -1)
{
break #Can’t find a match
}
$value = $CommandLine.Substring($index + 1,$closeIndex – ($index + 1))
[void]$Arguments.Add($value)
$index = $closeIndex

#Find First Quote
$index = $CommandLine.IndexOf('"',$index + 1)
}
}
return $Arguments
}

function Convert-CommandLineToDictionary
{
<#
.SYNOPSIS
Parses and converts the commandline of a packaged executable into a Dictionary

.DESCRIPTION
Parses and converts the commandline of a packaged executable into a Dictionary

.PARAMETER  Dictionary
The Dictionary to load the value pairs into.

.PARAMETER  CommandLine
The commandline of the package executable

.PARAMETER  ParamIndicator
The character used to indicate what is a parameter.

.EXAMPLE
$Dictionary = New-Object System.Collections.Specialized.StringDictionary
Convert-CommandLineToDictionary -Dictionary $Dictionary -CommandLine $Commandline  -ParamIndicator '-'
#>
Param(    [ValidateNotNull()]
[System.Collections.Specialized.StringDictionary]$Dictionary,
[string]$CommandLine,
[char] $ParamIndicator)

$Params = Parse-Commandline $CommandLine

for($index = 0; $index -lt $Params.Count; $index++)
{
[string]$param = $Params[$index]
#Clear the values
$key = ""
$value = ""

if($param.StartsWith($ParamIndicator))
{
#Remove the indicator
$key = $param.Remove(0,1)
if($index  + 1 -lt $Params.Count)
{
#Check if the next Argument is a parameter
[string]$param = $Params[$index + 1]
if($param.StartsWith($ParamIndicator) -ne $true )
{
#If it isn’t a parameter then set it as the value
$value = $param
$index++
}
}
$Dictionary[$key] = $value
}#else skip
}
}
function Get-ExclusionList
{
$REGExclusion = "HKLM:\Software\AppsNotify\AppsNotifyExclusion"
$excllist = @()
Log-Write -LogPath $logfilePath -LineValue "Start exclusion list creation"
If ($(Test-Path $REGExclusion))
{

##Discovery
$regKey = $REGExclusion

$p = Get-ItemProperty $REGExclusion

$p.PSObject.Properties |  foreach  { if (("PSPath","PSParentPath","PSChildName","PSDrive","PSProvider") -notcontains $_.name) { $excllist += $($_.name) } }

}
Log-Write -LogPath $logfilePath -LineValue $($excllist)
return $excllist
}

function Validate-IsURL
{
<#
.SYNOPSIS
Validates if input is an URL

.DESCRIPTION
Validates if input is an URL

.PARAMETER  Url
A string containing an URL address

.INPUTS
System.String

.OUTPUTS
System.Boolean
#>
[OutputType([Boolean])]
param ([string]$Url)

if($Url -eq $null)
{
return $false
}

return $Url -match "^(ht|f)tp(s?)\:\/\/[0-9a-zA-Z]([-.\w]*[0-9a-zA-Z])*(:(0-9)*)*(\/?)([a-zA-Z0-9\-\.\?\,\'\/\\\+&amp;%\$#_]*)?$"
}

function Get-CMUserApps {
[CmdletBinding()]
param
(
[Parameter(Mandatory=$True,
ValueFromPipelineByPropertyName=$True,
HelpMessage='URL for Application Catalogue')]
$url,
[Parameter(Mandatory=$True,
ValueFromPipelineByPropertyName=$True,
HelpMessage='Path to logfile')]
$logfile,
[Parameter(Mandatory=$True,
ValueFromPipelineByPropertyName=$True,
HelpMessage='Temp-file')]
$temp
)
Begin {
log-write -LogPath $logfile -LineValue "Create web service proxy"
$catalogurl = $url;
Log-Write -LogPath $logfile -LineValue "Connecting to $catalogurl"
try {
$url = $catalogurl+"/ApplicationViewService.asmx?WSDL";
$service = New-WebServiceProxy $url -UseDefaultCredential;

}
catch {
Log-Error -LogPath $logfile -ErrorDesc "AppCatalog no response" -ExitGraceFully $false
Log-Finish -LogPath $logfilePath -NoExit $true
break
}
}
Process {


$total = 0;
try {
Log-Write -LogPath $logfile -LineValue "Gathering applications"
$service.GetApplications("Name",$null,"Name","",100,0,$true,"PackageProgramName",$false,$null,[ref]$total) | select ApplicationId,Name | Export-Clixml $temp
return $true
}

catch {
Log-Error -LogPath $logfile -ErrorDesc $error[0] -ExitGraceFully $false
return $false
}
Remove-Variable -Name url
Remove-Variable -Name total
$service.dispose()

}

}

function Compare-CMUserApps {
[CmdletBinding()]
param
(
[Parameter(Mandatory=$True,
ValueFromPipelineByPropertyName=$True,
HelpMessage='Permanent-file')]
$file,
[Parameter(Mandatory=$True,
ValueFromPipelineByPropertyName=$True,
HelpMessage='Temp-file')]
$temp,
[Parameter(Mandatory=$True,
ValueFromPipelineByPropertyName=$True,
HelpMessage='Path to logfile')]
$logfile
)
Process {
Log-Write -LogPath $logfile -LineValue "Comparing applications lists"
If (-Not (Test-Path $file)) {
Log-Write -LogPath $logfile -LineValue "No previous version of apps list"
try {
Rename-Item $temp "$prefix apps.xml"
}
catch {
Remove-Item $temp
Log-Error -LogPath $logfile -ErrorDesc "Unable to create initial list" -ExitGracefully $false
}

}
Else {

Log-Write -LogPath $logfile -LineValue "Starting check......"
# $diffs = (Compare-Object -ReferenceObject $(Get-Content $file) -DifferenceObject $(Get-Content $temp)) | Where {$_.SideIndicator -eq '<='}
# $diffsserver = (Compare-Object -ReferenceObject $(Get-Content $file) -DifferenceObject $(Get-Content $temp)) | Where {$_.SideIndicator -eq '=>'}

If ((Compare-Object -ReferenceObject $(Get-Content $file -ReadCount 0) -DifferenceObject $(Get-Content $temp -ReadCount 0)) -eq $null) {
Log-Write -LogPath $logfile -LineValue "No new applications"
Log-Write -LogPath $logfile -LineValue "Removing temporary file"

try {
Remove-Item $temp
}
catch {

Log-Error -LogPath $logfile -ErrorDesc "Unable to remove temp list" -ExitGracefully $false
}

}
Elseif (((Compare-Object -ReferenceObject $(Get-Content $file -ReadCount 0) -DifferenceObject $(Get-Content $temp -ReadCount 0)) | Where {$_.SideIndicator -eq '<='}) -ne $null -and ((Compare-Object -ReferenceObject $(Get-Content $file -ReadCount 0) -DifferenceObject $(Get-Content $temp -ReadCount 0)) | Where {$_.SideIndicator -eq '=>'}) -eq $null ) {
Log-Write -LogPath $logfile -LineValue "Less applications received"
try {
Log-Write -LogPath $logfile -LineValue "Remove permanent list"
Remove-Item $file
}
catch {
Remove-Item $temp
Log-Error -LogPath $logfile -ErrorDesc "Unable to remove permanent list" -ExitGracefully $false
}

try {
Log-Write -LogPath $logfile -LineValue "Rename temporary list"
Rename-Item $temp "$prefix apps.xml"
}
catch {
Log-Error -LogPath $logfile -ErrorDesc "Unable to switch temp-list to permanent" -ExitGracefully $false
}

}

Else {
Log-Write -LogPath $logfile -LineValue "New applications found"
#              $lastWrite = (get-item $file).LastWriteTime
#              $timespan = new-timespan -days 0 -hours 4 -minutes 5
#
#                if (((get-date) - $lastWrite) -gt $timespan) {
#                    Log-Write -LogPath $logfile -LineValue "File is older than 4 h, will reset"
#
#                         try {
#                            Log-Write -LogPath $logfile -LineValue "Remove permanent list"
#                            Remove-Item $file
#                          }
#                          catch {
#                                  Remove-Item $temp
#                                  Log-Error -LogPath $logfile -ErrorDesc "Unable to remove permanent list" -ExitGracefully $false
#                          }
#
#                          try {
#                                  Log-Write -LogPath $logfile -LineValue "Rename temporary list"
#                                  Rename-Item $temp "$prefix apps.xml"
#                                  }
#                          catch {
#                                   Log-Error -LogPath $logfile -ErrorDesc "Unable to switch temp-list to permanent" -ExitGracefully $false
#                                }
#                }
#                Else {
$newapps = $true
#                }

}

}
If ($newapps -eq $true) {
return $True

}
#Remove-Variable * -ErrorAction 'SilentlyContinue'
#$error.Clear()
#Clear-Host
#$diffs = $null
#$diffsserver = $null
}
}

function OnApplicationLoad {
#Note: This function is not called in Projects
#Note: This function runs before the form is created
#Note: To get the script directory in the Packager use: Split-Path $hostinvocation.MyCommand.path
#Note: To get the console output in the Packager (Windows Mode) use: $ConsoleOutput (Type: System.Collections.ArrayList)
#Important: Form controls cannot be accessed in this function
#TODO: Add snapins and custom code to validate the application load



return $true #return true for success or false for failure
}

function Get-NewAppCatalogApps {

[CmdletBinding()]
param
(
[Parameter(Mandatory=$True,
ValueFromPipelineByPropertyName=$True,
HelpMessage='Permanent-file')]
$file,
[Parameter(Mandatory=$True,
ValueFromPipelineByPropertyName=$True,
HelpMessage='Temp-file')]
$temp,
[Parameter(Mandatory=$True,
ValueFromPipelineByPropertyName=$True,
HelpMessage='Path to logfile')]
$logfile
)

Process {


try {
$array = @(Compare-Object $(Get-content $file -ReadCount 0) $(Get-Content $temp -ReadCount 0) | Where {$_.SideIndicator -eq '=>' -and $_.InputObject -match '<S N="Name">'} |Select-Object $_ -ExpandProperty InputObject)
}
catch {
Log-Error -LogPath $logfile -ErrorDesc "Unable to list new applications" -ExitGracefully $false
return
}

[array]$exclusionlist = Get-ExclusionList
Log-Write -LogPath $logfilePath -LineValue "Exclusions:"
Log-Write -LogPath $logfilePath -LineValue "$($exclusionlist)"
$intApps = $array.Length - 4
Log-Write -LogPath $logfilePath -LineValue "User has $(4+$intApps) new applications"
$i = 0
$applist = ""
foreach ($element in $Array) {

$element = $element.TrimStart(' ')
$element = $element -replace "</S>","`n"
$element = $element -replace "<S N=`"Name`">",""

if ($exclusionlist -contains $($element -replace "`n",""))
{

Log-Write -LogPath $logfilePath -LineValue "$($element -replace   `"`n`",`"`") is now excluded"
$array.remove($element)
$intApps-=1
}
else
{
$i++

if ($i -lt "5")
{

#$element = $element.TrimStart(' ')
#$element = $element.TrimEnd('</S>')
#$element = $element -replace "</S>","`n"
#$element = $element -replace "<S N=`"Name`">",""
$applist += $element

#$applist = $applist.TrimStart(' ')
}
else {
$applist = $applist + "and $intApps more`n"
return $applist
}
}
}
return $applist
}

}

function OnApplicationExit {
#Note: This function is not called in Projects
#Note: This function runs after the form is closed
#TODO: Add custom code to clean up and unload snapins when the application exits
#Log-Finish -LogPath $logfilePath -NoExit $true
$script:ExitCode = 0 #Set the exit code for the Packager
Log-Finish -LogPath $logfilePath -NoExit $false
break
}

$AppNotify_Load={
#TODO: Initialize Form Controls here
$NotifyIcon.Text = $list
#$NotifyIcon.BalloonTipText = $list
$NotifyIcon.ShowBalloonTip(30000,"New Applications Available",$list, 'Info')
}

#region Control Helper Functions
function Show-NotifyIcon
{
<#
.SYNOPSIS
Displays a NotifyIcon's balloon tip message in the taskbar's notification area.

.DESCRIPTION
Displays a NotifyIcon's a balloon tip message in the taskbar's notification area.

.PARAMETER NotifyIcon
The NotifyIcon control that will be displayed.

.PARAMETER BalloonTipText
Sets the text to display in the balloon tip.

.PARAMETER BalloonTipTitle
Sets the Title to display in the balloon tip.

.PARAMETER BalloonTipIcon
The icon to display in the ballon tip.

.PARAMETER Timeout
The time the ToolTip Balloon will remain visible in milliseconds.
Default: 0 - Uses windows default.
#>
param(
[Parameter(Mandatory = $true, Position = 0)]
[ValidateNotNull()]
[System.Windows.Forms.NotifyIcon]$NotifyIcon,
[Parameter(Mandatory = $true, Position = 1)]
[ValidateNotNullOrEmpty()]
[String]$BalloonTipText,
[Parameter(Position = 2)]
[String]$BalloonTipTitle = '',
[Parameter(Position = 3)]
[System.Windows.Forms.ToolTipIcon]$BalloonTipIcon = 'None',
[Parameter(Position = 4)]
[int]$Timeout = 0
)

if($NotifyIcon.Icon -eq $null)
{
#Set a Default Icon otherwise the balloon will not show
$NotifyIcon.Icon = [System.Drawing.Icon]::ExtractAssociatedIcon([System.Windows.Forms.Application]::ExecutablePath)
}

$NotifyIcon.ShowBalloonTip($Timeout, $BalloonTipTitle, $BalloonTipText, $BalloonTipIcon)
}

#endregion

$NotifyIcon_MouseDoubleClick=[System.Windows.Forms.MouseEventHandler]{
#Event Argument: $_ = [System.Windows.Forms.MouseEventArgs]
#TODO: Place custom script here
Log-Write -LogPath $logfilePath -LineValue "User clicked icon"
Log-Write -LogPath $logfilePath -LineValue "Sending user to $appcatalog"
Start-Process $appcatalog

Log-Write -LogPath $logfilePath -LineValue "Timer started for $totaltime"
#Add TotalTime to current time
$script:StartTime = (Get-Date).AddSeconds($TotalTime)
#Start the timer
$timer1.Start()

}

$NotifyIcon_MouseClick=[System.Windows.Forms.MouseEventHandler]{
#Event Argument: $_ = [System.Windows.Forms.MouseEventArgs]
#$NotifyIcon.Visible = $true
$NotifyIcon.ShowBalloonTip(30000,"New Applications Available",$list, 'Info')
}

$NotifyIcon_BalloonTipClicked={
Log-Write -LogPath $logfilePath -LineValue "User clicked ballontip"
Log-Write -LogPath $logfilePath -LineValue "Sending user to $appcatalog"
Start-Process $appcatalog

Log-Write -LogPath $logfilePath -LineValue "Timer started for $totaltime"
#Add TotalTime to current time
$script:StartTime = (Get-Date).AddSeconds($TotalTime)
#Start the timer
$timer1.Start()

}

#Get path which scripts run from
$CurrentPath = Get-ScriptDirectory

#Import log-functions
#. "$CurrentPath\Logging_Functions.ps1"

&nbsp;

#Prefix for all generated files in user's %TEMP%
$prefix = "appsnotify"

#Logfile
$logfilePath = $env:temp+"\$prefix app.log"
#Visibility after icon-click
$TotalTime = 300
$TotalTime_All = 14300

if ((Get-WmiObject Win32_LogicalDisk -Filter "DeviceID='C:'" | Select-Object -ExpandProperty FreeSpace) -lt "20000000" ) {
exit
}

$check=Get-Process AppsNotify -ErrorAction SilentlyContinue | Measure-Object

if ($check.count -lt "2") {

}
else {
Log-Error -LogPath $logfilePath -ErrorDesc "AppsNotify is already running. Terminating. " -ExitGraceFully $false
exit
}

#Temporary file to store applications
$tempfilePath  = $env:temp+"\$prefix app_temp.xml"
#Permanent file to store applications
$filePath = $env:temp +"\$prefix apps.xml"
#Reset log-file for this session
Remove-Item $logfilePath

&nbsp;

################################################################################################################
Log-Start -LogPath $env:temp -LogName "$prefix app.log" -ScriptVersion "2.0"

&nbsp;

#Verify that the $CommandLine variable exists
if($CommandLine -ne $null -and $CommandLine -ne "")
{
#Log-Write -LogPath $logfilePath -LineValue "There is a command-line"
Log-Write -LogPath $logfilePath -LineValue "Command-line is:"
Log-Write -LogPath $logfilePath -LineValue "$CommandLine"
#$Arguments = Parse-Commandline $CommandLine
#Convert the Arguments. Use – as the Argument Indicator
$Dictionary = New-Object System.Collections.Specialized.StringDictionary
Convert-CommandLineToDictionary -Dictionary $Dictionary -CommandLine $Commandline  -ParamIndicator '-'
}
else
{
#Not running in a packager or no command line arguments passed
Log-Error -LogPath $logfilePath -ErrorDesc "No command-line argument. Use -appcatalog <url>" -ExitGraceFully $false
Log-Finish -LogPath $logfilePath -NoExit $false
break
}

$appcatalog = $Dictionary["appcatalog"]
if($appcatalog -ne $null -and $appcatalog -ne "") {
Log-Write -LogPath $logfilePath -LineValue "Passed Application Catalog is $appcatalog"
if (Validate-IsURL -Url $appcatalog) {
Log-Write -LogPath $logfilePath -LineValue "Passed Application Catalog is a URL"
}
Else {
Log-Error -LogPath $logfilePath -ErrorDesc "This is not a url" -ExitGraceFully $false
Log-Finish -LogPath $logfilePath -NoExit $false
break
}

}
else {
#Address to Application Catalogue
Log-Error -LogPath $logfilePath -ErrorDesc "We need an Application Catalog" -ExitGraceFully $false
Log-Finish -LogPath $logfilePath -NoExit $false
break
}

if ($(([PInvoke.Win32.UserInput]::IdleTime).TotalMinutes) -gt 10) {
Log-Write -LogPath $logfilePath -LineValue "Idle Time: $(([PInvoke.Win32.UserInput]::IdleTime).TotalMinutes)"
Log-Write -LogPath $logfilePath -LineValue "No user at computer, exiting"
Log-Finish -LogPath $logfilePath -NoExit $false
break
}

if ((Get-CMUserApps -url $appcatalog -logfile $logfilePath -temp $tempfilePath) -eq $true) {

if ((Compare-CMUserApps -file $filePath -temp $tempfilePath -logfile $logfilePath) -eq $true) {
$list = "$(Get-NewAppCatalogApps -file $filePath -temp $tempfilePath -logfile $logfilePath)"
if ($list)
{
Log-Write -LogPath $logfilePath -LineValue "Applist is $list"
#$NotifyIcon.Text = $list
try {

$NotifyIcon.Visible = $true
#Add TotalTime to current time
Log-Write -LogPath $logfilePath -LineValue "Starting general timer..."
$script:StartTime_all = (Get-Date).AddSeconds($TotalTime_All)
#Start the timer
$timer_all.Start()

}
catch {
Log-Error -LogPath $logfilePath -ErrorDesc "Tray icon failed..." -ExitGraceFully $false
Log-Finish -LogPath $logfilePath -NoExit $false
break
}
finally {
try {
Log-Write -LogPath $logfilePath -LineValue "Removing $filepath"
Remove-Item $filePath
Log-Write -LogPath $logfilePath -LineValue "Renaming $tempfilePath"
Rename-Item -Path "$tempfilePath"  -NewName "$prefix apps.xml" -Force

}
catch {
Remove-Item $tempfilePath
Log-Error -LogPath $logfile -ErrorDesc "Unable to remove permanent list" -ExitGracefully $false
}
}
}
else {
try {
Log-Write -LogPath $logfilePath -LineValue "Removing $filepath"
Remove-Item $filePath
Log-Write -LogPath $logfilePath -LineValue "Renaming $tempfilePath"
Rename-Item -Path "$tempfilePath"  -NewName "$prefix apps.xml" -Force

}
catch {
Remove-Item $tempfilePath
Log-Error -LogPath $logfile -ErrorDesc "Unable to remove permanent list" -ExitGracefully $false
}
Rename-Item -Path "$tempfilePath"  -NewName "$prefix apps.xml" -Force
Log-Finish -LogPath $logfilePath -NoExit $false
break
}

}
Else {
Log-Finish -LogPath $logfilePath -NoExit $false
break
}

}
Else {
Log-Finish -LogPath $logfilePath -NoExit $false
break
}

&nbsp;

&nbsp;

$NotifyIcon_BalloonTipShown={
#TODO: Place custom script here
Log-Write -LogPath $logfilePath -LineValue "Notifying user"
}

$timer1_Tick={
#Use Get-Date for Time Accuracy
[TimeSpan]$span = $script:StartTime - (Get-Date)

#Update the display
#$formSampleTimer.Text = $labelTime.Text = "{0:N0}" -f $span.TotalSeconds

if($span.TotalSeconds -le 0)
{
Log-Write -LogPath $logfilePath -LineValue "Timer has passed"
$timer1.Stop()
$NotifyIcon.Visible = $false
$AppNotify.Close()
$NotifyIcon.Dispose()
Log-Finish -LogPath $logfilePath -NoExit $true
}
}

$timer_all_Tick={
#TODO: Place custom script here
#Use Get-Date for Time Accuracy
[TimeSpan]$span = $script:StartTime_all - (Get-Date)

#Update the display
#$formSampleTimer.Text = $labelTime.Text = "{0:N0}" -f $span.TotalSeconds

if($span.TotalSeconds -le 0)
{
Log-Write -LogPath $logfilePath -LineValue "General timer is up.. closing..."
$timer_all.Stop()
$NotifyIcon.Visible = $false
$AppNotify.Close()
$NotifyIcon.Dispose()
Log-Finish -LogPath $logfilePath -NoExit $true
}
}

CM, IP-ranges and unknown networks

A follow-up of a previous post relating to matching ConfigMgr IP-range boundaries to known networks. The essence is to send an email (scheduled at your own interval) that notifies if there are any clients on unknown networks inventoried.

Prerequisites

  • Configuration Manager is required to have IP-ranges for boundaries
  • We assume that boundaries are /24, or 255.255.255.0 and IPv4
  • Clients need to collect and report Network Data
  • The user running the script needs to be able to connect and read data from Configuration Manager database
  • SMTP-server to send an email

What do we do?

  • Gather all active IP-range boundaries from database
  • Gather reported networks that matches the DNS-Suffix defined, IPv4 and sum up # of devices within /24-networks

Loads of assumptions….  incase you need to tweak it this is how we gather from the database.

Client networks – alter for $netquery. Remember to replace $dnsdomain

select 
SUBSTRING(ip.IPAddress0, 1, LEN(ip.IPAddress0) - CHARINDEX('.',REVERSE(ip.IPAddress0))) + '.1' As IP,
COUNT(*) as Devices
 from v_Network_DATA_Serialized as ip 
where ip.IPAddress0 IS NOT NULL and 
ip.IPSubnet0 != '64'  and 
ip.IPSubnet0 != '128' and 
ip.IPSubnet0 = '255.255.255.0' and 
ip.DNSDomain0 IS NOT NULL and 
ip.DNSDomain0 = '$dnsdomain' and 
ip.TimeStamp > DATEADD(day, -10, GETDATE()) 
GROUP BY  SUBSTRING(ip.IPAddress0, 1, LEN(ip.IPAddress0) - CHARINDEX('.',REVERSE(ip.IPAddress0))) 
ORDER BY Devices DESC

Boundaries – alter for $query

select 
bound.DisplayName, 
SUBSTRING(bound.value,1,CHARINDEX('-',bound.value) -1) AS LEFTHALF,
SUBSTRING(bound.value,CHARINDEX('-',bound.value) +1 ,100) AS RIGHTHALF 
from vSMS_Boundary as bound 
where 
bound.BoundaryType = '3' and 
bound.GroupCount > 0

Output – email

An email is sent with the following information after running the script

Network – network (always ends with a .1)
Devices – number of devices
DNSDomain – DNS-suffix
IPSubnet – network mask
DefaultGateway – default gateway
DHCPServer – DHCP-server

Parameters

Before running the actual script the following is required to be updated. DNSDomain – what DNS-suffix your clients are reporting as.

#Database params
$ErrorActionPreference = "silentlycontinue"
#Database-server
$datasource = "DBSERVER"
#Database
$database = "CM_DATABASE"
#DNS-Domain
$dnsdomain = 'dns.suffix.se'

#Email params
$EmailParams = @{
To         = 'email'
From       = 'email'
Smtpserver = 'smtp.company.se'
Subject    = "ConfigMgr Client Unknown Networks -  $(Get-Date -Format dd-MMM-yyyy)"
}

Script

#========================================================================
# Created with: Powershell ISE
# Created on:   2017-08-13
# Created by:   NiKa
# Organization:
# Filename:     CM_BoundaryCheck.ps1
#========================================================================

function IsIpAddressInRange {
param(
[string] $ipAddress,
[string] $fromAddress,
[string] $toAddress
)

$ip = [system.net.ipaddress]::Parse($ipAddress).GetAddressBytes()
[array]::Reverse($ip)
$ip = [system.BitConverter]::ToUInt32($ip, 0)

$from = [system.net.ipaddress]::Parse($fromAddress).GetAddressBytes()
[array]::Reverse($from)
$from = [system.BitConverter]::ToUInt32($from, 0)

$to = [system.net.ipaddress]::Parse($toAddress).GetAddressBytes()
[array]::Reverse($to)
$to = [system.BitConverter]::ToUInt32($to, 0)

$from -le $ip -and $ip -le $to
}

###### Parameters #################

#Database params
$ErrorActionPreference = "silentlycontinue"
#Database-server
$datasource = "DBSERVER"
#Database
$database = "CM_DATABASE"
#DNS-Domain
$dnsdomain = 'dns.suffix.se'

#Email params
$EmailParams = @{
To         = 'email'
From       = 'email'
Smtpserver = 'smtp.company.se'
Subject    = "ConfigMgr Client Unknown Networks -  $(Get-Date -Format dd-MMM-yyyy)"
}
###### Parameters #################

#### Retrieve client networks
$netquery = "select SUBSTRING(ip.IPAddress0, 1, LEN(ip.IPAddress0) - CHARINDEX('.',REVERSE(ip.IPAddress0))) + '.1' As IP, COUNT(*) as Devices from v_Network_DATA_Serialized as ip where ip.IPAddress0 IS NOT NULL and ip.IPSubnet0 != '64'  and ip.IPSubnet0 != '128' and ip.IPSubnet0 = '255.255.255.0' and ip.DNSDomain0 IS NOT NULL and ip.DNSDomain0 = '$dnsdomain' and ip.TimeStamp > DATEADD(day, -10, GETDATE()) GROUP BY  SUBSTRING(ip.IPAddress0, 1, LEN(ip.IPAddress0) - CHARINDEX('.',REVERSE(ip.IPAddress0))) ORDER BY Devices DESC"

$networks= Invoke-Sqlcmd -Query $netquery -server $datasource -Database $database

#### Retrieve boundaries
$query = "select bound.DisplayName, SUBSTRING(bound.value,1,CHARINDEX('-',bound.value) -1) AS LEFTHALF,SUBSTRING(bound.value,CHARINDEX('-',bound.value) +1 ,100) AS RIGHTHALF from vSMS_Boundary as bound where bound.BoundaryType = '3' and bound.GroupCount > 0"

$iprange = Invoke-Sqlcmd -Query $query -server $datasource -Database $database

#### Check if IP-address are within boundaries
$report=@()
foreach ($net in $networks) {
if (!($net.ip -eq '192.168.1.1' -or $net.ip -eq '0.0.0.1'  -or $net.ip -eq '10.10.0.1' -or $net.ip -eq '172.16.0.1' -or $net.ip -eq '169.254.43.1' -or $net.ip -eq '169.254.36.1' -or $net.ip -eq '192.168.0.1' -or $net.ip -eq '10.0.100.1')) {
$i = 0
$J = $iprange.count
$boundaryfound = $false
do {
#$iprange[$i].displayname
if (IsIpAddressInRange $net.ip $iprange[$i].LEFTHALF $iprange[$i].RIGHTHALF)
{

$boundaryfound = $true
}
$i++
} until ($i -gt $j)
if ($boundaryfound -eq $false)
{
#write-host "Network: $($net.ip) - Devices: $($net.Devices)"

#Retrieve information about network
$devquery = "select distinct DNSDomain0,IPSubnet0,DefaultIPGateway0,DHCPServer0 from v_Network_DATA_Serialized as ip
where ip.IPAddress0 IS NOT NULL
and ip.IPSubnet0 != '64'
and ip.TimeStamp > DATEADD(day, -10, GETDATE())
and ip.IPaddress0 like '$($($net.ip) -replace ".$")%'"
$devices= Invoke-Sqlcmd -Query $devquery -server $datasource -Database $database
$report += New-Object psobject -Property @{Network=$($net.ip);Devices=$($net.devices);DNSDomain=$($devices.DNSDomain0);IPSubnet=$($devices.IPSubnet0);DefaultGateway=$($devices.DefaultIPGateway0);DHCPServer=$($devices.DHCPServer0)}
}
}
}

if ($report -ne $null) {

#$report

#Generate email

$style = @"
<style>
body {
color:#333333;
font-family: ""Trebuchet MS"", Arial, Helvetica, sans-serif;}
}
h1 {
text-align:center;
}
h2 {
border-top:1px solid #666666;
}
table {
border-collapse: collapse;
font-family: ""Trebuchet MS"", Arial, Helvetica, sans-serif;
}
th {
font-size: 10pt;
text-align: left;
padding-top: 5px;
padding-bottom: 4px;
background-color: #1FE093;
color: #ffffff;
}
td {
font-size: 8pt;
border: 1px solid #1FE093;
padding: 3px 7px 2px 7px;
}
</style>

"@

$Properties = @(
'Network',
'Devices',
'DNSDomain',
'IPSubnet',
'DefaultGateway',
'DHCPServer'
)

$body = $report |
Select-Object -Property $Properties|
ConvertTo-Html -Head $style -Body "
<H3>Devices from unknown networks ($($results.Count))</H3>

" |
Out-String

Send-MailMessage @EmailParams -Body $Body -BodyAsHtml

}

Task Sequence and shutdown (not reboot) a computer and continue

For some reason there is a requirement to do a computer shutdown (not restart) while running a task sequence, and once the computer starts again there is a need to continue running the task sequence where we left it.

How do you go about that? Let’st start…

image

We need two scripts, a task sequence with the ability to run one script and then to start a task sequence controlled restart.

For testing purposes a networkshare was used instead of leveraging a package, but in real-life and in production – all of the files can be placed in a package and executed from there.
This concept is tested within WinPE (using Winpeutil etc…), but you can most likely adapt it to a Windows installation.

Run Monitor

The ‘Run Monitor’ step will kick off a VB-Script that will start a powershell script – and then exit. Simple enough to start a script, and then allow the task sequence to continue with the next steps

image

VBScript
Runapp "powershell.exe","-noprofile -executionpolicy bypass -file " & GetScriptPatH() & "shutdown.ps1"

Private Function RunApp(AppPath,Switches)
Dim WShell
Dim RunString
Dim RetVal
Dim Success

On Error Resume Next

Set WShell=CreateObject("WScript.Shell")

RunString=Chr(34) &AppPath & Chr(34) & " " & Switches
Retval=WShell.Run(RunString,0,False)

RunApp=Retval

Set WShell=Nothing
End Function

Private Function GetScriptPath
GetScriptPath=Replace(WScript.ScriptFullName,WScript.ScriptName,"")
End Function

The powershell-script (shutdown.ps1) looks as follows;

  1. Create a TS Environment (so we can read variables)
  2. Verify if the variable _SMSTSBootStagePath is set
  3. If the drive-part is longer than a single-letter – we know that the boot-image is prepared and that the reboot countdown has started.
Powershell
$end =$true
write-output "start"

DO
{
start-sleep 2
Get-date
#Remove-Variable -name tsenv -Force -ErrorAction SilentlyContinue
if (!$tsenv) {
try  {
$tsenv = New-Object -COMObject Microsoft.SMS.TSEnvironment
}
catch {
write-output "No TS started yet"
}
}
try  {
$bootpath = $tsenv.Value("_SMSTSBootStagePath") -split ":"
$tsenv.Value("_SMSTSBootStagePath")
if ($bootpath[0].length -gt 1) {
write-output "SMSTSBootStagePath prepped for reboot"
$end = $false
}
}
catch {
write-output "variable not set"
}

} While ($end -eq $true)

start-sleep 5

wpeutil shutdown

Restart

The restart step is fairly generic and you can configure it as you need. A thing to note is that the time-out needs to be higher than the start-sleep within the Powershell-script. As the purpose is to continue within WinPE – the step is configured to start to the boot-image.

image

Lenovo and management of BIOS settings

Lenovo has published an excellent guide for management of their BIOS settings via scripts for the ThinkPad-series. It seems that it does apply for all different series (ThinkCentre, ThinkPad, ThinkStation) and therefore the same methods can be used regardless of the type of device.

However, there are numerous caveats to the documentation and some minor misalignments of naming standards between specific device types – even within the same series of devices.

Supervisor Password

Initial password

Password seems to be quite odd for Lenovo. First of all – password can’t be set the first time around via their WMI interface but requires that someone sets on the device. In addition – the experience has been that depending on the type of keyboard (validated with a Lenovo and an HP set of keyboards) the password might not be set as expected. In the end – we could only validate what the password was (and use the expected password) when set with an HP-keyboard.

Updates settings with password

Once a password is set it becomes a requirement to pass this one when changing any setting, or setting a new password. To pass this one each updated setting requires the password, encoding and keyboard and in addition it is also required when saving the bios settings. One could find many more efficient methods, but this is the way togo about it. After lots of testing – the following methods have been succesful;

Changing a BIOS configuration

$wmi = Get-WmiObject -Class Lenovo_SetBiosSetting -Namespace root\wmi
$wmi.SetBiosSetting("TCG Security Feature,Active,password,ascii,us;")

Note that at the end there is a ;.
Ascii and us is the encoding and the language of the keyboard. This is the most common setup – so lets stick with it.

To save the settings the following command can be issued;

(Get-WmiObject -Class Lenovo_SaveBiosSettings -Namespace root\wmi).SaveBiosSettings(password,ascii,us;)

If you are unsure wether there is a password or not – we can always test and validate. If you configure all settings incorrectly and then try to save without the appropiate password (blank or with the correct password) – all settings are lost.

You can check if a password is set by using the following method;

$password = “,password,ascii,us”
$result = ((Get-WmiObject -Class Lenovo_SaveBiosSettings -Namespace root\wmi).SaveBiosSettings(password,ascii,us)).return
if (!($result –eq "Success")) {
#if the command isn’t successfull we set a blank password
$password = ""
}
if ($result –eq "Success") {
$nopass = $false
}

To avoid writing lots of code once we have identified if a password is in use – we can leverage the $password and append it to every settings.

$wmi.SetBiosSetting("TCG Security Feature,Active$password")

The $nopass can be used to choose decide how we save the settings

if ($nopass -eq $true) {
Get-WmiObject -Class Lenovo_SaveBiosSettings -Namespace root\wmi).SaveBiosSettings()
}
else {
(Get-WmiObject -Class Lenovo_SaveBiosSettings -Namespace root\wmi).SaveBiosSettings($passwordsave)
}
<pre>[sourcecode]

Settings

Unlike the harmonized and common way to handle BIOS settings via the WMI interface – settings have a wide spread of possible names and setting options. Quite often similiar enough to cause frustration

An overview of TPM related settings and Secureboot

image

In addition – once these settings are enabled they can’t be disabled. Lenovo has taken a secure-by-default stance and will force someone to physically access the computer to decrease security. As far as their guide states today the following settings can’t be disabled – once they are enabled – via WMI.

SecureBoot
SecureRollbackPrevention
PhysicalPresneceForTpmClear
PhysicalPresenceForTpmProvision

Copy a ConfigMgr Application DeploymentType

A small function inspired by Fred Bainbridges post on howto append an OS requirement to a deployment type. The purpose of the function is to copy the Deploymenttype within an application, but if someone feels like a spending a few hours to rewrite it to copy between different applications that could possible work aswell.

 

function Copy-CMAppDT {
<#
.SYNOPSIS
Copy a single Deployment Type within an application
.DESCRIPTION
This will create a copy of a DeploymentType, with the lowest priority and the name specified
.EXAMPLE
Copy-CMAppDT -appName "PingKing 2.0.0" -DeploymentType "PingKing 2.0.0" -newDTname "PingKing Updated" -siteCode P01 -siteServer CM01
.EXAMPLE
.PARAMETER appName
This is the name of the configmgr application that has the deployment type. This accepts input from pipeline.
.PARAMETER DeploymentType
This is the name of the Deployment Type that you want to copy.
.PARAMETER newDTName
This is the name of the new DeploymentType.
.PARAMETER siteCode
This the ConfigMgr site code you are working with. Defaults to LAB
.PARAMETER siteServer
This the site server you are going to working with.  WMI calls are made to this server.  It is most likely your primary site server.
#>
[CmdletBinding()]
param (
[Parameter(
Position=0,
Mandatory=$true,
ValueFromPipeline=$true,
ValueFromPipelineByPropertyName=$true)
]
$appName,
$DeploymentType,
$newDTname,
$siteCode = "LAB",
$siteServer = "cm01.cm.lab"
)
begin {
write-verbose "Import module"
import-module 'C:\Program Files (x86)\Microsoft Configuration Manager\AdminConsole\bin\ConfigurationManager.psd1' -force #make this work for you
write-verbose "Connect to Provider and change location"
if ((get-psdrive $sitecode -erroraction SilentlyContinue | measure).Count -ne 1) {
new-psdrive -Name $SiteCode -PSProvider "AdminUI.PS.Provider\CMSite" -Root $SiteServer
write-verbose "Connect to the default scope"
try {
$connectionManager = New-Object Microsoft.ConfigurationManagement.ManagementProvider.WqlQueryEngine.WqlConnectionManager
$connectionManager.Connect($siteServer) | Out-Null
[Microsoft.ConfigurationManagement.ApplicationManagement.NamedObject]::DefaultScope = [Microsoft.ConfigurationManagement.AdminConsole.AppManFoundation.ApplicationFactory]::GetAuthoringScope($connectionManager)
}
catch {
throw-error "$error[0]"
}
}
write-verbose "Set location $sitecode"
set-location $sitecode`:

}

process {
write-verbose "Get Application $appName"
try {
$Appdt = Get-CMApplication -Name $appName
}
catch {
throw "Unable to get $appName - $error[0]"
}

$xml = [Microsoft.ConfigurationManagement.ApplicationManagement.Serialization.SccmSerializer]::DeserializeFromString($appdt.SDMPackageXML,$True)

$numDTS = $xml.DeploymentTypes.count
write-verbose "Number of DT: $numDTS"
$dts = $xml.DeploymentTypes

foreach ($dt in $dts)
{
if ($dt.title -eq $DeploymentType ) {
write-verbose "Found DT $deploymenttype"
$newDeploymentType = $dt.Copy()
write-verbose "Set new DT name $newDTname"
$newDeploymentType.Title = $newDTname
$newDeploymentType.ChangeID()

}
}
if ($newDeploymentType.GetType().name -eq 'DeploymentType') {

write-verbose "New DT created"
$xml.DeploymentTypes.Add($newDeploymentType)

write-verbose "Commit to AppObject"
$UpdatedXML = [Microsoft.ConfigurationManagement.ApplicationManagement.Serialization.SccmSerializer]::SerializeToString($XML, $True)
$appdt.SDMPackageXML = $UpdatedXML
Set-CMApplication -InputObject $appDT
}
else {
write-error "No DeploymentType $newDTname located"
}
}

end
{
write-verbose "Return to c:"
set-location c:
}
}

App-V 5 and publishing error code: 040000002C.

A minor defect that causes a publishing failure for any packages (only tested for publishing towards a user though. The error code looks like this;

Publish-AppvClientPackage : Application Virtualization Service failed to
complete requested operation.
Operation attempted: Publish AppV Package.
AppV Error Code: 040000002C.
Error module: Virtualization Manager. Internal error detail: 4FC086040000002C.

There seems to already be a few discussions online that assists in resolving the with a few different methods – one seems to suggest to delete a registry key and there is a one that contains a more granular approach by resetting the registry values under LocalVFSSecuredFolders.

A correct view is that each SID under this registry key references the %USERPROFILE%.

image

and incorrect (and the cause of the error) references the Default-user profile

image

A quick script (which you can wrap in a Compliance Item or a script – or whatever the preference is..) to remediate this. The actual fix (Set-ItemProperty) is prefixed with # – please test it before you deploy it.

$users=@()
$return = 0
$users = ($k = gi HKLM:\SOFTWARE\Microsoft\AppV\client\Virtualization\LocalVFSSecuredUsers).GetValueNames() | % {

New-Object PSObject -Property @{

Name = $_

Type = $k.GetValueKind($_)

Value = $k.GetValue($_)

} | select Name, Type, Value

}

foreach ($u in $users) {
if ($u.value -eq 'c:\users\Default\AppData\Local\Microsoft\AppV\Client\VFS') {
$return = 1
#Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\AppV\client\Virtualization\LocalVFSSecuredUsers' -Name $($u.Name) -value '%USERPROFILE%\AppData\Local\Microsoft\AppV\Client\VFS'
}
}
$return

 

 

Per the above forum post this should be resolved within App-V 5.0 SP3, however I have still seen minor occurances for later releases – so I wouldn’t call that a  confirmed fix.