Wednesday 19 December 2018

Windows 10 NLA Public vs Private profiles - what's the difference?

In a recent post I described how I used Powershell to configure a dual-homed Radius server where I wanted to firewall everything on the DMZ interface but not affect the production interface.  I did this using a Windows feature known as NLA – Network Location Awareness – which has been around in one form or another since Windows XP, although many people still know very little about it.
NLA in Windows 10 uses 3 different network profiles: Domain, Public and Private.  Windows assigns the network connection to one of these profiles when a new network is discovered.  It’s important to know the differences because this actually provides us with a really powerful tool to lock down our machines using the built in Windows Firewall.

How the appropriate location is determined

Domain

Microsoft explain that Windows checks the connection specific DNS name against “HKEY_Local_Machine\Software\Microsoft\Windows\CurrentVersion\Group Policy\History\NetworkName” (although on my test machine this was an empty key but “HKEY_Local_Machine\Software\Microsoft\Windows\CurrentVersion\Group Policy\History\MachineDomain” contained the domain DNS name).  If this matches and the machine is able to go on and contact a Domain Controller via LDAP, then you are assigned the Domain profile.

Public vs Private

This is the bit most people get confused about and it is a distinction which appeared from Windows Vista onwards (in XP the profiles were Domain and Standard).  The way that the location is determined is via the prompt that you receive when connecting to a new network ie “Do you want to allow your PC to be discoverable by other PCs and devices on this network?”.  Selecting “Yes” assigns the Private profile whilst “No” assigns the Public profile.

It’s useful to know of this distinction as it will allow you to configure specific rules on the firewall which will behave differently depending on whether you are connected to a trusted or untrusted network.

Wednesday 12 December 2018

Connect to O365 exchange with powershell using session import

 

It seems fairly obvious but a lot of people are unsure how to import a remote powershell session to O365. Simply do the following:

 

 

$Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://outlook.office365.com/powershell-liveid/ -Credential $UserCredential -Authentication Basic -AllowRedirection

Import-PSSession $Session

 

 

 

Friday 30 November 2018

Disable inbound advanced firewall rules on public interface only with Powershell for a Windows Radius/NPS server

 

 

Recently I built an NPS server for Radius authentication, and had to dual home it with one NIC in the DMZ and one on the production network.  I wanted to firewall everything on the DMZ interface but not affect the production interface.  That way I could then allow the traffic I wanted to enable on the DMZ interface port by port.  Assuming windows has correctly detected the profile on the two interfaces as “Domain” and “Public”, which it should based on the resources visible on each network, you can run the following script to disable traffic just on the public interface.

 

 

 

$LogFilePath = $env:LOCALAPPDATA + "\Cloudwyse\Logs\adv_firewall" + $(get-date -Format ddMMyy_HHmmss) + ".log"

Start-Transcript -Path $LogFilePath -NoClobber

 

$rules = Get-NetFirewallRule

$total = 0

foreach ($rule in $rules) {

if (($rule.Profile -like "any" -or $rule.Profile -match "public") -and $rule.enabled -like "True" -and $rule.direction -like "Inbound") {

if ($rule.Profile -like "any") {

  Set-NetFirewallRule -Name $rule.Name -Profile "Domain, Private"

  write-host "Setting" $rule.DisplayName "Domain, Private" }

elseif ($rule.Profile -match "Domain" -and $rule.Profile -match "private" -and  $rule.Profile -match "public" ) {

  Set-NetFirewallRule -Name $rule.Name -Profile "Domain, Private"

  write-host "Setting" $rule.DisplayName "Domain, Private" }

elseif ($rule.Profile -match "Domain" -and  $rule.Profile -match "public" ) {

  Set-NetFirewallRule -Name $rule.Name -Profile "Domain"

  write-host "Setting" $rule.DisplayName "Domain" }

elseif ($rule.Profile -match "Private" -and  $rule.Profile -match "public" ) {

  Set-NetFirewallRule -Name $rule.Name -Profile "Private"

  write-host "Setting" $rule.DisplayName "Private" }

elseif ($rule.Profile -like "public" ) {

  Disable-NetFirewallRule -Name $rule.Name

  write-host "Disabling" $rule.DisplayName }

else {write-host -ForegroundColor Red "Error - check logs"}

$total = $total +1  }}

write-host -ForegroundColor Yellow "$total rules processed"

 

stop-transcript

 

 

 

 

Monday 19 November 2018

Very Simple Powershell Ping test or IP scanner script

 

 

This simple Powershell script will carry out a quick check against each IP in a 24 bit subnet and return a value of true or false dependent on whether or not it receives a reply.  It’s similar to using an IP scanner or a ping script.

 

 

$subnet = "10.20.6."

1..254 | Foreach-Object {write-host "$Subnet$_..." (Test-Connection -ComputerName "$Subnet$_" -Quiet -Count 1 ) }

 

 

It’s simple and does the job as it is however you could obviously make it more fancy by exporting to csv or an array or emailing the output etc etc.

 

 

Wednesday 17 October 2018

O365 - reverting hard matching migration using ImmutableID

 

This is a very obscure problem, so I’m recording this more for my own reference in future rather than expecting anyone else to have the same issue!

The issue occurs when a migration from a hybrid exchange domain to another domain which uses AD Sync has been completed in the following manner:

1.    filter/delete user in current domain

2.    AD Sync soft deletes mailbox

3.    Create user in new domain (in a filtered OU that won’t be synchronised)

4.    Obtain new account GUID and convert to immutableID string (base64)

5.    Undelete mailbox (mailbox becomes cloud mailbox)

6.    Assign ImmutableID to mailbox (from the target account)

7.    Move target account to a synchronised OU then allow AD sync to hard match the accounts

8.    For some reason, there is a need to reverse this migration.  So filter/delete user in new domain

9.    AD Sync soft deletes mailbox

10. Re-create or unfilter user in old hybrid domain

11. Obtain account GUID and convert to immutableID string (base64)

12. Undelete mailbox (mailbox becomes cloud mailbox)

13. Assign ImmutableID to mailbox (from the original account) using the command

  

Set-MsolUser -UserPrincipalName "<UPN>" -ImmutableId "<ImmutableID>"

 

 

At this point the following error is received:

 

Set-MsolUser : Uniqueness violation. Property: SourceAnchor.

At line:1 char:1

+ Set-MsolUser -UserPrincipalName user@domain.com  -Immutableid

 

 

The fix is to run…

 

 

Get-MsolUser -ReturnDeletedUsers | select-object UserPrincipalName,Immutableid,objected

 

 

Find the user with the ImmutableID matching the one you are trying to assign

 

 

Remove-MsolUser -objectID "<objectID>" -RemoveFromRecycleBin

 

 

Then you should be able to run

 

 

Set-MsolUser -UserPrincipalName "<UPN>" -ImmutableId "<ImmutableID>"

 

 

…if you still have a problem, find the user’s objectID with

 

 

Get-MsolUser -userprincipalname "<UPN>" | select-object UserPrincipalName,Objectid

 

 

And run…

 

 

Set-MsolUser -objectid "<objectID" -ImmutableId "<ImmutableID>"

 

 

 

 

Friday 28 September 2018

SCCM Task Sequence Windows 10, with Dell CCTK, UEFI, BitLocker, powershell renaming script

 

BIOS-to-UEFI group – this group will only run if the machine has booted from BIOS rather than UEFI

 

I’ve then added a message specific for a certain model of laptops that require a BIOS downgrade for reasons I’ll not go into.  The selection criteria as a WMI query and the message box is displayed underneath.

This is quite a useful way to abandon a task sequence, reboot and display a message to the user.

powershell.exe -command (new-object -ComObject Microsoft.SMS.TsProgressUI).CloseProgressDialog() ; (new-object -ComObject wscript.shell).Popup('Your message.',0,'Your message title',0x0 + 0x30) ; Exit 1

Next, if the task sequence wasn’t launched from PXE, reboot to WinPE

Prepare the disk

The next group runs only if it’s a Dell machine

The next step checks that it’s a 64bit machine.  As I haven’t yet come across any x86 machines in this environment, they will just display a message box saying they’re not supported.  If required, the x64 group can be duplicated and modified for x86 machnes.  The WMI filters are as follows:

(64 Bit) select * from Win32_Processor where DeviceID="CPU0" and AddressWidth="64"

(32 Bit) select * from Win32_Processor where DeviceID="CPU0" and AddressWidth="32"

You will notice each of the following steps use a naming convention such as “x64 > Legacy > UEFI - Enable UEFI” to show the location of the given step.  This just makes troubleshooting the task sequence easier.

It’s necessary to use the Dell CCTK tool to change BIOS settings on these laptops.  Unfortunately there are a number of different CCTK tools, each of which is restricted to certain hardware types:

x86 – Standard

x86 – Legacy

x64 – Standard

x64 – Legacy

As all our machines are x64 we don’t need to worry about the x86 version.  But I’ve split my task sequence here into Legacy and Non-Legacy, based on the outcome of the CCTK test.  Thanks to the guys at scconfigmgr.com whose steps and script I used as a basis for my script.

The script will create a TS Variable “IsLegacy” which will take a Boolean value from the script.  This will determine whether the legacy or non-legacy group runs in the next step.

 

 

# Functions

BEGIN

{

  try {

    $TSEnvironment = New-Object -ComObject Microsoft.SMS.TSEnvironment -ErrorAction Continue

  }

  catch [System.Exception] {

    Write-Warning -Message "Unable to construct Microsoft.SMS.TSEnvironment object"; break

  }

}

PROCESS {

  # Set Logs Directory

  $LogsDirectory = Join-Path -Path $TSEnvironment.Value("_SMSTSLogPath") -ChildPath ""

 

  function Write-CMLogEntry {

    param (

      [parameter(Mandatory = $true, HelpMessage = "Value added to the log file.")]

      [ValidateNotNullOrEmpty()]

      [string]$Value,

      [parameter(Mandatory = $true, HelpMessage = "Severity for the log entry. 1 for Informational, 2 for Warning and 3 for Error.")]

      [ValidateNotNullOrEmpty()]

      [ValidateSet("1", "2", "3")]

      [string]$Severity,

      [parameter(Mandatory = $false, HelpMessage = "Name of the log file that the entry will written to.")]

      [ValidateNotNullOrEmpty()]

      [string]$FileName = "ApplyDellCCTK.log"

    )

    # Determine log file location

    $LogFilePath = Join-Path -Path $LogsDirectory -ChildPath $FileName

   

    # Construct time stamp for log entry

    $Time = -join @((Get-Date -Format "HH:mm:ss.fff"), "+", (Get-WmiObject -Class Win32_TimeZone | Select-Object -ExpandProperty Bias))

   

    # Construct date for log entry

    $Date = (Get-Date -Format "MM-dd-yyyy")

   

    # Construct context for log entry

    $Context = $([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)

   

    # Construct final log entry

    $LogText = "<![LOG[$($Value)]LOG]!><time=""$($Time)"" date=""$($Date)"" component=""ApplyDellCCTK"" context=""$($Context)"" type=""$($Severity)"" thread=""$($PID)"" file="""">"

   

    # Add value to log file

    try {

      Out-File -InputObject $LogText -Append -NoClobber -Encoding Default -FilePath $LogFilePath -ErrorAction Stop

    }

    catch [System.Exception] {

      Write-Warning -Message "Unable to append log entry to ApplyDellCCTK.log file. Error message at line $($_.InvocationInfo.ScriptLineNumber): $($_.Exception.Message)"

    }

  }

 

  Write-CMLogEntry -Value "Staritng Dell CCTK compatibility check" -Severity 1

  $CCTKVersion = (Get-ItemProperty .\CCTK.exe | select -ExpandProperty VersionInfo).ProductVersion

  Write-CMLogEntry -Value "Running Dell CCTK version $CCTKVersion on host system" -Severity 1

  $CCTKExitCode = (Start-Process cctk.exe -Wait -PassThru).ExitCode

  Write-CMLogEntry -Value "Reading Dell CCTK running output" -Severity 1

  if (($CCTKExitCode -eq "141") -or ($CCTKExitCode -eq "140")) {

    Write-CMLogEntry -Value "Non WMI-ACPI BIOS detected. Setting CCTK legacy mode" -Severity 2

    $LegacyCond = "True"

  }

  else {

    Write-CMLogEntry -Value "WMI-ACPI BIOS detected" -Severity 1

    $LegacyCond = "False"

  }

  Write-CMLogEntry -Value "Setting LegacyCond task sequence variable" -Severity 1

  $TSEnvironment.Value("IsLegacy") = $LegacyCond

  Write-CMLogEntry -Value "Legacy condition is $LegacyCond" -Severity 2

}

 

 

  

In the next step a short script runs, utilising the IsLegacy variable to determine which version of the WinPE HAPI Driver to install (which is necessary to execute cctk.exe).

 

 

 

@echo off

REM Determine Arch

IF "%ISLEGACY%" == "TRUE" GOTO :LEGACYTRUE

GOTO LEGACYFALSE

 

:LEGACYTRUE

legacy\hapi\hapint64.exe -i -k C-C-T-K -p "hapint64.exe"

GOTO END

 

:LEGACYFALSE

hapi\hapint.exe -i -k C-C-T-K -p "hapint.exe"

GOTO END

:END

 

 

The next group will run is IsLegacy is set to “True” (if it’s false, the Non-Legacy group will run later, which is exactly the same, except it runs the cctk.exe at the root, not the one at legacy\cctk.exe)

 

The next step sets the BIOS password.  This step is set to ‘Continue on error’ which is what would happen if the BIOS password is already set (in this environment, I know that if there is a password set it will be set to this value).

 

The next step disables the UEFI network stack which is necessary in order to allow the laptop to boot from HDD after the restart.  We will re-enable the network stack later.

 

The group runs only if we haven’t booted from UEFI.

 

Now we change the bootorder

 

Disable legacy ROMs

 

Enable secure boot

 

Format as GPT (necessary for Windows 10)

 

Reboot

 

The next group only runs if the TPM is NOT activated and enabled

 

Next I run a quick powershell script to identify if this is a VM and add it to a TS variable called “PSisVM” (all my VMs are Hyper-V so this would need amending to support VMware).  This variable helps later on.

 

 

 

# Functions

BEGIN

{

  try {

    $TSEnvironment = New-Object -ComObject Microsoft.SMS.TSEnvironment -ErrorAction Continue

  }

  catch [System.Exception] {

    Write-Warning -Message "Unable to construct Microsoft.SMS.TSEnvironment object"; break

  }

}

PROCESS {

  # Set Logs Directory

  $LogsDirectory = Join-Path -Path $TSEnvironment.Value("_SMSTSLogPath") -ChildPath ""

 

  function Write-CMLogEntry {

    param (

      [parameter(Mandatory = $true, HelpMessage = "Value added to the log file.")]

      [ValidateNotNullOrEmpty()]

      [string]$Value,

      [parameter(Mandatory = $true, HelpMessage = "Severity for the log entry. 1 for Informational, 2 for Warning and 3 for Error.")]

      [ValidateNotNullOrEmpty()]

      [ValidateSet("1", "2", "3")]

      [string]$Severity,

      [parameter(Mandatory = $false, HelpMessage = "Name of the log file that the entry will written to.")]

      [ValidateNotNullOrEmpty()]

      [string]$FileName = "CheckVM.log"

    )

    # Determine log file location

    $LogFilePath = Join-Path -Path $LogsDirectory -ChildPath $FileName

   

    # Construct time stamp for log entry

    $Time = -join @((Get-Date -Format "HH:mm:ss.fff"), "+", (Get-WmiObject -Class Win32_TimeZone | Select-Object -ExpandProperty Bias))

   

    # Construct date for log entry

    $Date = (Get-Date -Format "MM-dd-yyyy")

   

    # Construct context for log entry

    $Context = $([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)

   

    # Construct final log entry

    $LogText = "<![LOG[$($Value)]LOG]!><time=""$($Time)"" date=""$($Date)"" component=""CheckVM"" context=""$($Context)"" type=""$($Severity)"" thread=""$($PID)"" file="""">"

   

    # Add value to log file

    try {

      Out-File -InputObject $LogText -Append -NoClobber -Encoding Default -FilePath $LogFilePath -ErrorAction Stop

    }

    catch [System.Exception] {

      Write-Warning -Message "Unable to append log entry to CheckVM.log file. Error message at line $($_.InvocationInfo.ScriptLineNumber): $($_.Exception.Message)"

    }

  }

 

 

 

  Write-CMLogEntry -Value "Checking if this is a virtual machine" -Severity 1

  $sysMan = (Get-wmiobject -query "Select Manufacturer from Win32_SystemEnclosure") | select-object -expand Manufacturer

  Write-CMLogEntry -Value "Query returned $sysMan" -Severity 1

 

  IF($sysMan -like "*Microsoft*") {

       $PSisVM = "True"

       Write-CMLogEntry -Value "Virtual machine detected." -Severity 2

       } ELSE {

       $PSisVM = "False"

       Write-CMLogEntry -Value "Physical machine detected" -Severity 1

       }

 

  $TSEnvironment.Value("PSisVM") = $PSisVM

  Write-CMLogEntry -Value "PSisVM task sequence variable has been set to $PSisVM" -Severity 2

}

 

 

 

Next I run the Dell CCTK check again (we rebooted since the last time) and filter my legacy/non-legacy groups as before.

 

Next we enable UEFI PXE option (only if PSisVM is False… I’ve also filtered out a certain laptop model here).

 

Enable TPM (again – there is a WMI query filter where PSisVM = False)

 

Activate TPM (PSisVM = False)

 

Reboot

 

Next we create our final partitions with following conditions

 

Pre-provision BitLocker

 

Next I use a script to name my computers as I want them named.  This script is documented here so I’m not going to repeat the detail.

 

Apply the OS image (the unattend.xml just contains regional settings – happy to share if anyone needs it).

 

Apply Windows settings

 

Apply Network settings

 

Finally install the configmgr client

 

…and enable BitLocker with the following options

 

Filtered to ensure it isn’t a Windows To Go Device or a VM

 

Finally I install my packaged applications and carry out some post installation tasks such as copying some install info using a powershell script as follows

 

 

 

 

$regPath = "HKLM:\Software\Cloudwyse\BuildInfo"

$TSEnv = New-Object -COMObject Microsoft.SMS.TSEnvironment

$DateTime = (Get-Date).ToString('dd/MM/yy hh:mm')

$AdvertisementID = $TSEnv.value("_SMSTSAdvertID")

$Organisation = $TSEnv.value("_SMSTSOrgName")

$TaskSequenceID = $TSEnv.value("_SMSTSPackageID")

$Packagename = $TSEnv.value("_SMSTSPackageName")

$MediaType = $TSEnv.value("_SMSTSMediaType")

$TSVersion = $TSEnv.value("TSVersion")

$ClientVersion = $TSEnv.value("Clientversion")

$MachineName = $TSEnv.value("_SMSTSMachineName")

$Installationmode = $TSEnv.value("_SMSTSLaunchMode")

 

 

New-Item -Path $regPath -Force | Out-Null

New-ItemProperty -Path $regPath -Name "Date" -Value $DateTime -PropertyType String -Force | Out-Null

New-ItemProperty -Path $regPath -Name "Advertisement ID" -Value $AdvertisementID -PropertyType String -Force | Out-Null

New-ItemProperty -Path $regPath -Name "Organisation" -Value $Organisation -PropertyType String -Force | Out-Null

New-ItemProperty -Path $regPath -Name "Task SequenceID" -Value $TaskSequenceID -PropertyType String -Force | Out-Null

New-ItemProperty -Path $regPath -Name "Package Name" -Value $Packagename -PropertyType String -Force | Out-Null

New-ItemProperty -Path $regPath -Name "Media Type" -Value $MediaType -PropertyType String -Force | Out-Null

New-ItemProperty -Path $regPath -Name "TS Version" -Value $TSVersion -PropertyType String -Force | Out-Null

New-ItemProperty -Path $regPath -Name "Client Version" -Value $ClientVersion -PropertyType String -Force | Out-Null

New-ItemProperty -Path $regPath -Name "Machine Name" -Value $MachineName -PropertyType String -Force | Out-Null

New-ItemProperty -Path $regPath -Name "Installation Mode" -Value $Installationmode -PropertyType String -Force | Out-Null