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

Bitlocker and howto make your users love you

This is how you deploy Bitlocker and make everyone fall in love with you.

1. Don’t understand Bitlocker – and read some generic Microsoft guideline which provides you with a generic approach and doesn’t compensate for your lack of understanding

2. Don’t read Adams detailed walk-through, and in particular skip the section regarding PCR Settings if you are deploying this in a pre-Windows 10 / SecureBoot era.
(or the gist: enable PCR validation: 0, 1, 8, 9, 10, & 11 only  for legacy BIOS)

3. Really don’t make an effort to push this forward to a SecureBoot era where the annoyance for all users are minimal.

4. Don’t validate any hardware – any BIOS-versions, TPM versions or anything that could potentially have an impact on the experience of Bitlocker.

5. Don’t test anything and just assume that it works as all the guidelines that say you “must” do this will never have any negative impact (and is there a section which says impact? don’t read it)
(Interactive logon: Machine account lockout threshold should match your account lockout setting and also not be as low any given user will force the machine into recovery mode every single day)

6. If the a user is forced to provide a Bitlocker Recovery Key – don’t reset the platform validation data. Most likely the Bitlocker Recovery key will not show up during the next reboot.

7.  Make sure you configure stuff – especially things that contradict the initial setup state of Bitlocker. Future assumptions made by Microsoft will surely not impact you.

Windows 10 20H2 and Edge

To start of this blog-post we have to set a few basics…

Image result for edge chromium

Windows 10 20H2 includes Edge Chromium. Specifically – Edge Chromium v84 (something something)

If you deploy Edge Chromium later version (like 87? 88?) to your 1909/2004 devices – and then upgrade to Windows 10 20H2, you will effectively downgrade Edge Chromium to 84.

As far as I can understand – the details, the workarounds and apparently a promise that this will be better is all published on Borncity.

The issue at hand though is that Edge Chromium is a moving target, so including even the latest version today in whatever upgrade process you have – will most likely in a months time have an older version than what you have installed on your endpoint. This is installer will bomb-out with “there is a newer version already installed”. Yet, the users will be stuck with Edge Chromium 84. I don’t quite get why this is the case. The previous version is installed, but version 84 always starts and you can’t upgrade because of this and for some reason Edge Chromium is special software that just doesn’t tag along?

Now, to workaround this you can read the information regarding the installed Edge Chromium (it is installed, just not running) and then perform an over-ride (REINSTALL=ALL REINSTALLMODE=A according to the comments on Borncity).

Let’s gather what we need to create something simplistic.

Function to retrieve installed software in Powershell. There are a bunch out there. You can use PSADT. I just found one that was small and did the trick. I can see so many problems with it – but it works. Unfortunately I have no idea where I stole this from. If you want todo properly – use PSADT. If I stole this from you – post a comment and I will remove it and post a link instead.

function Get-InstalledSoftware {
    <#
    .SYNOPSIS
        Retrieves a list of all software installed
    .EXAMPLE
        Get-InstalledSoftware
        
        This example retrieves all software installed on the local computer
    .PARAMETER Name
        The software title you'd like to limit the query to.
    #>
    [OutputType([System.Management.Automation.PSObject])]
    [CmdletBinding()]
    param (
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$Name
    )
 
    $UninstallKeys = "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall", "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall"
    $null = New-PSDrive -Name HKU -PSProvider Registry -Root Registry::HKEY_USERS
    $UninstallKeys += Get-ChildItem HKU: -ErrorAction SilentlyContinue | Where-Object { $_.Name -match 'S-\d-\d+-(\d+-){1,14}\d+$' } | ForEach-Object { "HKU:\$($_.PSChildName)\Software\Microsoft\Windows\CurrentVersion\Uninstall" }
    if (-not $UninstallKeys) {
        Write-Verbose -Message 'No software registry keys found'
    } else {
        foreach ($UninstallKey in $UninstallKeys) {
            if ($PSBoundParameters.ContainsKey('Name')) {
                $WhereBlock = { ($_.PSChildName -match '^{[A-Z0-9]{8}-([A-Z0-9]{4}-){3}[A-Z0-9]{12}}$') -and ($_.GetValue('DisplayName') -like "$Name*") }
            } else {
                $WhereBlock = { ($_.PSChildName -match '^{[A-Z0-9]{8}-([A-Z0-9]{4}-){3}[A-Z0-9]{12}}$') -and ($_.GetValue('DisplayName')) }
            }
            $gciParams = @{
                Path        = $UninstallKey
                ErrorAction = 'SilentlyContinue'
            }
            $selectProperties = @(
                @{n='GUID'; e={$_.PSChildName}}, 
                @{n='Name'; e={$_.GetValue('DisplayName')}}
            )
            Get-ChildItem @gciParams | Where $WhereBlock | Select-Object -Property $selectProperties
        }
    }
}

Second you need something to validate that MSIExec isn’t busy. Like the function Test-IsMutexAvailable from (you guessed it) PSADT. Come to think of it – you really should just throw everything I did in a garbage bin and rewrite it using the PSADT framework.

Third – here is the crude and basic logic of what we want to run after the Windows 10 20H2 upgrade is completed.

Consists of – arguments to run the re-install of Edge. Oddly – the latest version should have a ProductCode, but not version 84 which comes in the box. Then wait until MSIExec had its coffee. Once coffee is up – run the install…. Logging optional.

$args = "/i `"$((Get-InstalledSoftware -Name "Microsoft Edge").guid)`" /qn REINSTALL=ALL REINSTALLMODE=A"
Test-IsMutexAvailable -MutexName 'Global\_MSIExecute' -MutexWaitTimeInMilliseconds (New-TimeSpan -Minutes 5).TotalMilliseconds
$process = Start-Process -FilePath "msiexec" -ArgumentList  $args -wait -PassThru

Set-Content -Path c:\windows\temp\edge.txt -Value "Edge Exit code: $($process.exitcode) - Argument: $($args)"

This should run if the upgrade is successful. Not entirely sure how this works in reality – but Microsoft offers a success.cmd since 2004 so that would be a good idea to use.

Defender for Endpoint – Whats the user count?

Whats the user count for users actually logged onto your devices – looking through Defender For Endpoint?

Quick-glance;

DeviceLogonEvents
| where AccountDomain == "YOURDOMAIN"
| where LogonType in ("Interactive","CachedInteractive") and ActionType == "LogonSuccess"
| extend parsed = parse_json(AdditionalFields)
| extend Localcheck = tostring(parsed.IsLocalLogon)
| where Localcheck notcontains "false"
| summarize AccountName=dcount(AccountName) by AccountDomain

CMPivot and SMB1

Ned Pyle has ensured there is a Event-log that details any attempts to communicate with SMB1 (incase this still is enabled on your endpoint). It exists both for SMBServer and SMBClient

See his great post for specifics regarding the event;

As of Configuration Manager (or MECM) 1910 you can utilize CMPivot to query all Event-logs (previously only a subset where available is only the Get-WinEventLog cmdl:et was used) – including SMBClient/Audit.

Sample query – summarized the number of events 30 days backwards per client

WinEvent('Microsoft-Windows-SmbClient/Audit', 30d) 
| where ID == 32002
| summarize count() by Device

Sample query – device, date and message

WinEvent('Microsoft-Windows-SmbClient/Audit', 30d) 
| where ID == 32002
| project device, datetime, Message

In addition you can create a collection of the clients you found;

Or if it needs to be pretty;

WinEvent('Microsoft-Windows-SmbClient/Audit', 30d) 
| where ID == 32002
| summarize count() by Device 
| render barchart with (kind=stacked, title='SMB1 Events', ytitle='Events')

Troubleshoot Office crashes – quick guide

Wrote these notes on how so many Office issues were solved. Sadly – this still applies – and a recent thread from Twitter reminded me that it might be useful.

Printer

Temporarily set the printer to ‘PDFCreator’ as default printer

Verify if the issue is resolved

Addins

Start the application in safemode or without addins

Sample:

winword.exe /a

excel.exe /s

outlook.exe /safe

If the application doesn’t crash if the Office application is started without addins, verify what addins the user has installed.

Narrow down the issue and attempt to identify which addin is causing the crash

You can review installed addins by selecting;

File -> Options

Review the Addins-option

Temporarily disable addins by using the Manage -> Go.. at the bottom

Some addins may require that you temporarily start the application as Administrator.

Profile

Close all Office applications

Registry issues

Try to temporarily rename the settings for a specific application in registry

Open regedit.exe

Locate the;

HKEY_CURRENT_USER\Software\Microsoft\office

Locate the crashing application;

Word, Excel, PowerPoint

Sample Path;

HKEY_CURRENT_USER\Software\Microsoft\Office\Word

Rename the registry key for your application – using Word as a sample;

word_temp 

Retry to start the application

Repeat the same steps for the specific application

HKEY_CURRENT_USER\Software\Microsoft\Office

Locate the specific version;

12.0 -> 2007

15.0 -> 2013

16.0 -> 2016

If the issue is not resolved, rename the version registry key – using 12.0 as a sample;

12.0_temp

If the issue is not resolved by temporarily renaming registry keys it is recommended to restore all registry-keys to their original name

Files

You can temporarily rename application specific folders for Office-applications. Suggestion is to rename the below folders to _temp and verify if the issue is resolved

%APPDATA%\Microsoft\Word

%APPDATA%\Microsoft\Excel

%APPDATA%\Microsoft\PowerPoint

%APPDATA%\Microsoft\Templates

%APPDATA%\Microsoft\Outlook

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."
                     }

                }
             }

WSUS Cleanup

Despite multiple articles of howto maintain a WSUS database for performance and scalability – there was always a performance issue dragging across which made all the cleanup jobs take forever. An earlier (not able to find it again) Technet Forum post clarified that this was due to all clean-up jobs beeing dependent on the Stored Procedure spDeleteUpdate, and this generates a temporary table that doesnt have an index for the appropiate columns. Unfortunately I werent able to find this post again – but lo, and behold – Microsoft confirmed the behaviour!

The fix is simply to alter the Stored Procedure spDeleteUpdate and append the following line during the creation of the temporary table;

image

EnableFastFirstSignin – howto set it up

EnableFastFirstSignin seems to be a semi-announced feature that is only possible to configure using a Provisioning Package. I think the official documentation states that it is in preview.

But, let’s not care about that

What is it?

This:

Is there  any documentation for this seemingly awesome black magic?

Yes, at Docs@Microsoft

How do I set it up?

Prerequisites

Windows 10 – 1809

Windows ADK – 1809 – Windows Imaging and Design Configuration

Brief overview of what you need todo;

· Configure Provisioning Package

· Generate Provisioning Package

· Install Provisioning Package

Configure / Generate package

Start Windows Imaging and Configuration Designer
Press to create Advanced Provisioning
Define the following settings:
Runtime Settings -> Policies -> Authentication ->

EnableFastFirstSignIn

Select Enabled

Runtime Settings -> SharedPC -> EnabledSharedPCMode

Select TRUE

Runtime Settings -> SharedPC -> AccountManagement -> AccountModel

Select Domain-joined only

Select Export -> Provisioning Package
Enter information regarding name of package, version. Information is arbitrarily set.

Owner is: IT Admin

Rank: 0 or 1

Press Next

Press Next
Select where to save the Provisioing Package

Press Next

Press Build
Press Finish

Install Package

Ensure you are using Windows 10 – 1809
Open an elevated Powershell prompt, using a local administrative account
NOTE: PackagePath is unique to the package name and environment you are working

Execute the following command to install:

Install-ProvisioningPackage -PackagePath “sample-name.ppkg” -QuietInstall

After this the awesome experience should be on whatever endpoint you installed this on. As far as I can tell all that remains is Group Policy Object processing.

What happens in the background?

What does this magical black box of awesomeness actually do in the background? Microsoft has little to reveal, however quite a few people have posted findings on Twitter so far

Trenteye seem to be digging into this further and this is what has been shared so far;

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
                }
         }
    }

}