Tagged: Active Directory

Default Powershell Execution Policy

You can use a GPO to set the ExecutionPolicy to a static value on all machines.

But what if you want to default it to something and then let the users have the ability to change it?

Group Policy Preferences is the easy answer.

Create a GPO targeting your machines and then create a entry under Computer Configuration -> Preferences -> Windows Settings -> Registry that looks like this:
130917-execpolicy-1

130917-execpolicy-2

The paths are:
Key: SOFTWARE\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell
Value Name: ExecutionPolicy
ValueData: RemoteSigned

With this setup a machine that doesn’t have any specific exec policy (then the reg key doesn’t exist) setup will get RemoteSigned.
(Another way is to do this with a startup script for the computer)

Bitlocker Info

I’m playing around with pipelining information to functions… and since I needed a function to read Bitlocker information from Active Directory, why not create one. 🙂

Function Get-BitlockerInfo {
	PARAM (
		[Parameter(Mandatory=$true,Position=0,ValueFromPipeline=$true)] $Computer
	)

	BEGIN {
		$bitLockerInfo = @()
	}
	
	PROCESS {
		Write-Verbose "Searching $($Computer.DistinguishedName) ..."
		Get-ADObject -LdapFilter "(msFVE-Recoverypassword=*)" -Searchbase $Computer.DistinguishedName -properties msFVE-RecoveryPassword | ForEach-Object {
			$Bitlocker = $_.Name.Split("{")
			$retObj = New-Object -TypeName System.Object
			$retObj | add-Member -memberType NoteProperty -name ComputerDistinguishedName -Value $Computer.DistinguishedName
			$retObj | add-Member -memberType NoteProperty -name BitlockerTime -Value $Bitlocker[0]
			$retObj | add-Member -memberType NoteProperty -name PasswordID -Value $Bitlocker[1].Replace("}", "")
			$retObj | add-Member -memberType NoteProperty -name RecoveryPassword -Value $_."msFVE-RecoveryPassword"

			$bitLockerInfo += $retObj
		}
	}
	
	END {
		Return $bitLockerInfo
	}
}

# Here is how to use it
Get-AdComputer -LdapFilter "(name=WKS012*)" | Get-BitlockerInfo | Format-List

User pictures in AD

Adding pictures to Active Directory is kind of nice, it gives you pictures in Outlook, Lync and few other products…

To add it you need to load a picture as binary and put it in to the attribute thumbnailPhoto on the user.

Function Add-AdThumbnailPhoto {
    PARAM (
        [ValidateScript({Test-Path $_ -PathType Leaf})] [string] $PicturePath,
        $UserAccount
    )
    
    If (!(Test-IsModuleLoaded "ActiveDirectory")) {
        Throw "You need to run: Import-Module ActiveDirectory"
    }
    Write-Verbose "Adding $($PicturePath) to $($UserAccount)"
    $pictureBinary = [byte[]](Get-Content $PicturePath -Encoding byte)
    
    If ([System.Text.Encoding]::ASCII.GetString($pictureBinary).Length -ge 100Kb) {
        Throw "Picture to large, max size is 100Kb"
    }
    
    
    Try {
        Set-AdUser $UserAccount -Replace @{ thumbnailPhoto = $pictureBinary }
    }
    Catch {
        Throw $error[0]
    }
}

Then you can use the function like this:

Add-AdThumbnailPhoto -PicturePath "C:\MyPicture.jpg" -UserAccount "MyUserAccount"

If you like to do it on a oneliner… here is a bit more compressed version of the same code:

Set-AdUser "MyUserAccount" -Replace @{ thumbnailPhoto = ([byte[]](Get-Content "C:\MyPicture.jpg" -Encoding byte) }

Logon Scripts in Powershell – Part2: LDAP-Queries

Next up… running LDAP in pure Powershell.
It would of course be nice to use ActiveDirectory cmdlets in the logonscript… but you probably don’t deploy those to all machines.

So, running LDAP queries can be done using a ADSI Searcher. One drawback with using this techniqe is the lack of site awareness… so if you have a larger network (or user out on low performing WAN-links) you need to take care of this “manually”.

The easy way to do this is by using the environment variable LogonServer… and if you have your AD-Sites set up right, this should be the closest server.

Here is how you can run LDAP-Queries on the LogonServer:

Function Get-AdLogonServer {
	<#
	.SYNOPSIS 
		Returns the AD logon server
	#>

	Return [string] (Get-ChildItem ENV:LogonServer).Value.ToUpper().Replace("\", "")
}

Function Run-LdapQuery {
	<#
	.SYNOPSIS
		Run a LDAP-query on logonserver
	#>
	PARAM (
		[string] $ldapFilter,
		[int] $pageSize = 512,
		[switch] $findOne
	)

	Write-Verbose "Run-LdapQuery: Running: LDAP://$(Get-AdLogonServer)/$($ldapFilter)"

	$AdSearch=([ADSISearcher]([ADSI]("LDAP://$(Get-AdLogonServer)")))
	$AdSearch.Filter = $ldapFilter
	if ($findOne.IsPresent) {
		$AdSearch.pagesize = 1
		Return $AdSearch.FindOne()
	} else {
		$AdSearch.pagesize = $pageSize
		Return $AdSearch.FindAll()
	}
}

And a quick sample to get the current users DN-string:

Function Get-UserDN {
	<#
	.SYNOPSIS
		Return a DN-string for a user (default to current user)
	#>
	PARAM (
		[string] $UserName = (Get-UserName)
	)

	Run-LdapQuery -ldapFilter "(sAMAccountName=$($UserName))" -findOne | ForEach-Object {
		$userDN = $_.Properties.Item("DistinguishedName")
	}

	Return $userDN
}

Logon Scripts in Powershell – Part1: Network Locations

Long time no blog…

Since I have written the logonscript we use (of course in Powershell).. why not share some snippets. 🙂

Starting out with the most common ones that “everyone” use. Map shares on file servers…
I prefer to create Network Shortcuts instead of using mapped drives.
Why?
Take a “standard” service desk call:
User: Hey, I want to access X:
Operator: Ok… where does it point?
User: I don’t know, but John Doe has a X: and I need it.
* Operator search for John Doe, checks all groups and how they are configured *
Operator: Ok… found it, log off and on and you are good to go.
User: But what will happen to my X: that I have today?

So, if you use drive letters you are bound to a specific number of letters and when you use X: for multiple of shares you will sooner or later end up in big mess…

Anyway, I will start of by showing you how to create the Network Locations.

(I will continue with another part on how the AD can be used to make this a lot easier than writing the shares, names and groups directly in the script)

So, here we go… a few good-to-have functions:

Function Get-NethoodPath {
	<#
	.SYNOPSIS 
		Returns path to "Network Locations"
		C:\Users\riro\AppData\Roaming\Microsoft\Windows\Network Shortcuts
	#>

	$folderNETHOOD = 0x13
	Return ((New-Object -com Shell.Application).Namespace($folderNETHOOD)).Self.Path
}

Function Delete-NethoodShortcut {
	<#
	.SYNOPSIS 
		Deletes a shortcut under Network Locations
	#>
	PARAM (
		[string] $LinkName
	)

	$NetLocLocalPath = Join-Path (Get-NethoodPath) $LinkName

	If (Test-Path $NetLocLocalPath) {
		Write-Verbose "Delete-NethoodShortcut: Removing: $($NetLocLocalPath)"
		Remove-Item $NetLocLocalPath -Force -Recurse
	}
}

Function Create-NethoodShortcut {
	<#
	.SYNOPSIS 
		Creates a shortcut under Network Locations
	#>
	PARAM (
		[string] $LinkTarget,
		[string] $LinkName,
		[string] $Description = "Folder on the network",
		$IconLocation = "%SystemRoot%\system32\SHELL32.DLL",
		$IconIndex = 9
	)
	
	Write-Verbose "Create-NethoodShortcut: Nethood shortcut: '$($LinkName)' targeting '$($LinkTarget)'"

	$NetLocLocalPath = Join-Path (Get-NethoodPath) $LinkName

	If (Test-Path "$($NetLocLocalPath).lnk") {
		Write-Verbose "Create-NethoodShortcut: Removing old link: $($NetLocLocalPath).lnk"
		Remove-Item "$($NetLocLocalPath).lnk" -Force
	}

	New-Item $NetLocLocalPath -Type directory -Force | Out-Null
	attrib "$NetLocLocalPath" +R

	if ( !(Test-Path "$NetLocLocalPath\Desktop.ini" -pathType leaf) ) {
		$oFile = new-Item "$NetLocLocalPath\Desktop.ini" -type file -force
		add-Content $oFile "[.ShellClassInfo]"
		add-Content $oFile "CLSID2={0AFACED1-E828-11D1-9187-B532F1E9575D}"
		add-Content $oFile "Flags=2"
		attrib "$NetLocLocalPath\Desktop.ini" +H +S -A
	}

	$wshShell = new-object -comobject wscript.shell
	$shortCut = $wshShell.CreateShortcut($NetLocLocalPath + "\target.lnk")
	$shortCut.TargetPath = $LinkTarget
	$shortCut.IconLocation = "$($IconLocation), $($IconIndex)"
	$shortCut.Description = $Description
	$shortCut.Save()
}

OK.. so you want to map diskdrives instead? This can also be done…

Function Test-DriveLetterInUse {
	<#
	.SYNOPSIS
		Return True if a disk is in use
		Test-Path doesnt work to good on UNC-mapped disks
	#>
	PARAM (
		[char] $DriveLetter
	)
	If ((Get-PSDrive -PSProvider FileSystem | Where {$_.Root -eq "$($DriveLetter):\"}) -eq $null) {
		Return $false
	} else {
		Return $true
	}
}

Function Remove-MappedDrive {
	PARAM (
		[char] $DriveLetter
	)

	If ( (Test-DriveLetterInUse -DriveLetter $DriveLetter) -eq $true ) {
		$wshNetwork = New-Object -com WScript.Network
		Write-Verbose "Remove-MappedDrive: $($DriveLetter): in use, removing drivemap"
		$wshNetwork.RemoveNetworkDrive("$($DriveLetter):")
	}
}

Function Map-UncPathToDrive {
	PARAM (
		[string] $UncPath,
		[char] $DriveLetter,
		[bool] $OverwriteExisting = $true
	)
	$wshNetwork = New-Object -com WScript.Network

	If ( (Test-DriveLetterInUse -DriveLetter $DriveLetter) -eq $true -and $OverwriteExisting -eq $true) {
		Write-Verbose "Map-UncPathToDrive: $($DriveLetter): in use, removing current drivemap"
		$wshNetwork.RemoveNetworkDrive("$($DriveLetter):")
	}
	
	If ((Test-DriveLetterInUse -DriveLetter $DriveLetter) -eq $false) {
		Write-Verbose "Map-UncPathToDrive: Mapping drive $($DriveLetter): to $($UncPath)"

		$wshNetwork.MapNetworkDrive("$($DriveLetter):", "$($UncPath)")
	} else {
		Write-Verbose "Map-UncPathToDrive: $($DriveLetter): allready in use"
	}
}

Keep a list of user/computer groups in registry

In the migration to ConfigMgr 2012 we are aiming at just keeping one collection for software distribution and instead rely on global conditions.
We do want to keep the option to add a user to a group and with that do a automagic distribution.

So, the examples below are snippets from the logonscript.

Here is the short version:
– Recursive search for group membership for user
– If group-list changed, update the reg key
(Then do the same for computer groups)
– If there are any changes to groups, trigger the ConfigMgr agent

– Create a global condition in ConfigMgr
– Use the condition like “If MyGlobalCondition contains SwDistGroupName”

$userDN = "CN=MyUser,OU=Users,DC=snowland,DC=se"
$computerDN = "CN=CyComputer,OU=Computers,DC=snowland,DC=se"

Function Get-DistGroups {
	PARAM (
		$ObjectDN,
		$nameFilter = "SwDist_*"
	)

	$distGroups = @()
	$AdSearch=([ADSISearcher]"LDAP://")
	$AdSearch.pagesize=512
	$AdSearch.Filter="(&(member:1.2.840.113556.1.4.1941:=$($ObjectDN))(name=$($nameFilter)))"
	$AdSearch.findAll() | ForEach-Object {
		Write-Verbose "Found group: $($_.Properties.Item('Name'))"
		$distGroups += $_.Properties.Item("Name")
	}

	Return $distGroups
}

# Standard setting
$runConfigMgrActions = $false

# Store DIST groups in registry
Write-Host "Reading User DIST groups"
$userDistGroups = Get-DistGroups -ObjectDN $userDN | Sort-Object
$CurrKeys = Read-RegKey -Key "HKCU:\Software\SSAB\SoftwareDistribution" -Name "UserGroups" | Sort-Object
If ((Compare-Object -ReferenceObject $CurrKeys -DifferenceObject $userDistGroups) -ne $null) {
	Write-Host "Found added/removed user groups, writing new list to registry"
	New-ItemProperty "HKCU:\Software\snowland\SoftwareDistribution" -Name "UserGroups" -Value $userDistGroups -PropertyType "MultiString" -Force | Out-Null

	$runConfigMgrActions = $true
} else {
	Write-Verbose "No changes in user groups"
}

Write-Host "Reading computer groups"
$computerDistGroups = Get-DistGroups -ObjectDN $computerDN | Sort-Object
$CurrKeys = Read-RegKey -Key "HKLM:\Software\SSAB\SoftwareDistribution" -Name "ComputerGroups" | Sort-Object

If ((Compare-Object -ReferenceObject $CurrKeys -DifferenceObject $computerDistGroups) -ne $null) {
	Write-Host "Found added/removed computer groups, writing new list to registry"
	New-ItemProperty "HKLM:\Software\snowland\SoftwareDistribution" -Name "ComputerGroups" -Value $computerDistGroups -PropertyType "MultiString" -Force | Out-Null
	$runConfigMgrActions = $true
} else {
	Write-Verbose "No changes in computer groups"
}

# Trigger ConfigMgr client actions if groups have changed
If ($runConfigMgrActions -eq $true) {
	Write-Verbose "Changes to DIST groups, running ConfigMgr Actions"
	(New-Object -ComObject CPApplet.cpAppletMgr).GetClientActions() | Where-Object {$_.Name -like "Application Global Evaluation Task*" -or $_.Name -like "Request & Evaluate*"} | Sort-Object Name | ForEach-Object {
		Write-Host "Starting ConfigMgr action: $($_.Name)"
		$_.PerformAction()
	}
}

(To do this you need to set a security GPO to the HKLM-key so that your users are allowed to write there.)

Create GPOs with Powershell

We are in the process of migrating to a brand spankin new Active Directory … and since it’s new there are no GPOs yet.

To automate and keep a strict naming convention we will use a self service portal to create GPOs.
This portal will have a few dropdown-boxes with options to minimize the risk of an user not creating the GPO as we want…

Anyway. This portal will fire a Powershell script that actualy creates the GPO and sets a bunch of things on it.

This script will:

  • Creates an AD-group
  • Creates an GPO
  • Remove Authenticated Users from GPO Security Filtering
  • Add a Administrator-group to the GPO
  • Adds a group with editing access to the GPO
  • Add the AD-Group created in the first step to Security Filtering on GPO
  • Disable Policy Computer/User Settings depending on the GPO scope
  • Add GPO-link to a Computer- or User-OU

Actually our script will give a few other groups and services (Advanced Group Policy Management – AGPM – to give one example) access to the GPOs and we create a Test-GPO as well… but I guess this is a good start for many of you.

PARAM (
	[string] $gpoScope = "U",
	[string] $gpoDescription = "PowershellTesting01",
	[string] $groupPrefix = "MyPrefix_L_",

	[string] $groupPath = "OU=All Groups,DC=snowland,DC=se",
	[string] $gpoLinkPathC = "OU=All Computers,DC=snowland,DC=se",
	[string] $gpoLinkPathU = "OU=All Users,DC=snowland,DC=se",
	
	[string] $gpoAdminsitrators = "MyPrefix_L_Role-GPO-Administrators",
	[string] $gpoEditors = "MyPrefix_L_Role-GPO-Editors"
)

Import-Module GroupPolicy
Import-Module ActiveDirectory

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
$gpoName = "GPO-$($gpoScope)-$($gpoDescription)"
$adGroupName = "$($groupPrefix)$($gpoName)"
$domainName = (Get-ADDomain).NetBIOSName
$dcServer = (Get-ADDomaincontroller).HostName

Write-Host "Settings:" -ForegroundColor Cyan
Write-Host "   AD GroupName       : $($adGroupName)" -ForegroundColor Cyan
Write-Host "   GPO Name           : $($gpoName)" -ForegroundColor Cyan
Write-Host "   GPO Prod           : $($gpoNameProd)" -ForegroundColor Cyan
Write-Host "   GPO Scope          : $($gpoScope)" -ForegroundColor Cyan
Write-Host "   Domain Controller  : $($dcServer)" -ForegroundColor Cyan
Write-Host "   Domain Name        : $($domainName)" -ForegroundColor Cyan
Write-Host "" -ForegroundColor Cyan

Write-Host "AD: Create AD group -" -ForegroundColor Cyan
New-ADGroup -Name $adGroupName -Description "GPO $($gpoScope) $($gpoDescription)" -GroupScope DomainLocal -Path $groupPath -Server $dcServer

Write-Host "Policy: Create policy" -ForegroundColor Cyan
New-GPO -Name $gpoName -Comment "$($gpoScope) $($gpoDescription)" -Server $dcServer

Write-Host "10 second pause to give AD a chanse to catch up" -ForegroundColor Cyan
Start-Sleep -Seconds 10

Write-Host "Remove Authenticated Users from GPO Security Filtering" -ForegroundColor Cyan
Set-GPPermissions -Name $gpoName -PermissionLevel None -TargetName "Authenticated Users" -TargetType Group -Server $dcServer

Write-Host "Add Administrators to GPO" -ForegroundColor Cyan
Set-GPPermissions -Name $gpoName -PermissionLevel GpoEditDeleteModifySecurity -TargetName $gpoAdminsitrators -TargetType group -Server $dcServer

Write-Host "Add Editors to GPO" -ForegroundColor Cyan
Set-GPPermissions -Name $gpoName -PermissionLevel GpoEdit -TargetName $gpoEditors -TargetType group -Server $dcServer

Write-Host "Add AD-Group to Security Filtering on GPO" -ForegroundColor Cyan
Set-GPPermissions -Name $gpoName -PermissionLevel GpoApply -TargetName "$($adGroupName)" -TargetType Group -Server $dcServer

If ($gpoScope -eq "C") {
	Write-Host "Disable Policy User Settings" -ForegroundColor Cyan
	(Get-GPO -Name $gpoName -Server $dcServer).GpoStatus = "UserSettingsDisabled"

	Write-Host "Add GPO-link to Computer OU" -ForegroundColor Cyan
	New-GPLink -Name $gpoName -Target $gpoLinkPathC -LinkEnabled Yes -Server $dcServer
} else {
	Write-Host "Disable Policy Computer Settings" -ForegroundColor Cyan
	(Get-GPO -Name $gpoName -Server $dcServer).GpoStatus = "ComputerSettingsDisabled"

	Write-Host "Add GPO-link to User OU" -ForegroundColor Cyan
	New-GPLink -Name $gpoName -Target $gpoLinkPathU -LinkEnabled Yes -Server $dcServer
}
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Write-Host "" -ForegroundColor Cyan
Write-Host "Done!" -ForegroundColor Cyan

Now I only need to figure out how to get AGPM to take control of the GPO …

AD Topology Diagrammer

Looks like a cool tool… haven’t had the time to test it… yet.

The Microsoft Active Directory Topology Diagrammer reads an Active Directory configuration using ActiveX Data Objects (ADO), and then automatically generates a Visio diagram of your Active Directory and /or your Exchange 200x Server topology. The diagramms include domains, sites, servers, administrative groups, routing groups and connectors and can be changed manually in Visio if needed.

Microsoft Active Directory Topology Diagrammer

Klurigheter med AD MP…

Stötte precis på ett litet bekymmer med AD MP’t. Om man inte vill övervaka samtliga AD-servrar i ett AD (alltså inte ha agenter på alla servrar) så får man lite felmeddelanden om att replikeringen inte fungerar.

Varför? Jo, efter “lite” script-läsande så komm jag fram till att:
Repl-scriptet går lokalt på samtliga DCs och:
– Skapar/uppdaterar ett servernamn’s-record under MOMLatencyMonitors i ADt
– Söker genom ADt efter vilka DCs som finns och kollar att deras servernamn’s-record inte är skapade för förlänge sedan

Vad betyder då det? Har man inte en agent på DC02 och DC01 kör scriptet så kommer den att se att DC02 finns i ADt men att servernamns-recordet inte finns och då skickar den ut ett felmeddelande.

Måttligt bra, men så är det. Jag skulle gärna se att den larmar på ett annorlunda sätt (ex. information-alert) när det inte finns något servernamns-record.

Nåja, det är ett script… script är till för att hackas 😉

I scriptet “AD Replication Monitoring” kan man förändra sub’en ReplCheck så att den inte tar med ett antal servrar i replikerings-övervakningen.

' Orginal-rad
'      strQuery = "<" & strLDAPSearchComputer & "CN=" & MONITORING_CONTAINER_NAME & "," & strRoot & ">;(objectCategory=container);whenChanged,adminDescription,cn;oneLevel"

' Förändrad till (Exkludering av av servrar med namn SERVERNAMNET*)
      strQuery = "<" & strLDAPSearchComputer & "CN=" & MONITORING_CONTAINER_NAME & "," & strRoot & ">;(&(objectCategory=container)(!cn=SERVERNAMNET*));whenChanged,adminDescription,cn;oneLevel"