Skip to main content

Introducing pstools.queue

Hannes Palmquist

Hannes Palmquist

Senior Consultant Cloud

Recently I developed a tool that utilizes the .NET class system.collections.queue. The function runs continously and produces workitems that should be processed in sequence and in the same order they were added. One simple way to acheive this is to use the collection queue. This class provides a simple way to add items to the queue and retreive the oldest item when needed.

For fun I wanted to create a wrapper for the queue class so that it can be used in a powershell style syntax. Additionally i missed functionality to see the rate and velocity that items were added and removed to the queue.

Queue objects created with this module contains metrics for count of added and removed items, rate of added and removed items and velocity.

Repo
Docs
PowerShell Gallery

Pester, mock remote or unavailable cmdlets

Hannes Palmquist

Hannes Palmquist

Senior Consultant Cloud

There are a lot of forum post of people having a hard time mocking some cmdlets, either it is remote cmdlets imported by a session that is not available during testing, cmdlets that are not present on the development machine or simply some built in cmdlets that are hard to mock like Import-PSSession, Invoke-Command etc.

Mocking a cmdlet that are unavailable on the development machine is quite straight forward by just declaring a dummy function before the mock.

function Get-Foo {
$Mailboxes = Get-Mailbox
}
Describe -Name "Foo" -Fixture {
BeforeAll {
function Get-Mailbox {}
Mock Get-Mailbox -MockWith {
# return fake mailbox object
}
}
It -Name "Bar" -Test {
{ Get-Foo } | should -not -throw
}
}

The same method can be applied when mocking a built in cmdlet that will fail parameter validation which happends even though the cmdlets is mocked

function Get-Foo {
$Session = New-PSSession
Import-PSSession -Session $Session
}
Describe -Name "Foo" -Fixture {
BeforeAll {
Mock New-PSSession -MockWith {}
Mock Import-PSSession -MockWith {}
}
It -Name "Bar" -Test {
{ Get-Foo } | should -not -throw
}
}
[-] Foo.Bar 51ms (49ms|1ms)
Expected no exception to be thrown, but an exception "Cannot validate argument on parameter 'Session'. The argument is null. Provide a valid value for the argument, and then try running the command again." was thrown from C:\Users\hanpalmq\OneDrive - Crayon Group\Desktop\temp.ps1:3 char:31
+ Import-PSSession -Session $Session
+ ~~~~~~~~.
at { Get-Foo } | should -not -throw, C:\Users\hanpalmq\OneDrive - Crayon Group\Desktop\temp.ps1:12
at <ScriptBlock>, C:\Users\hanpalmq\OneDrive - Crayon Group\Desktop\temp.ps1:12
Tests completed in 214ms
Tests Passed: 0, Failed: 1, Skipped: 0 NotRun: 0

The error states that Import-PSSession requires valid PSSession object to be passed to the session parameter. Again we need to define a dummy function that don't expects a PSSession object to be passed.

function Get-Foo {
$Session = New-PSSession
Import-PSSession -Session $Session
}
Describe -Name "Foo" -Fixture {
BeforeAll {
function Import-PSSession {}
Mock New-PSSession -MockWith {}
Mock Import-PSSession -MockWith {}
}
It -Name "Bar" -Test {
{ Get-Foo } | should -not -throw
}
}

Convert-DateStringToDateTimeObject

Hannes Palmquist

Hannes Palmquist

Senior Consultant Cloud

Eventually you will stumble upon badly formatted date/time strings that you need to parse. Instead of doing a lot of Split, Trim, Substring and what not you can leverage the DateTime class methods Parse/TryParse/ParseExact. To PS-ify the use of this method I wrote this powershell function that can take any date/time string and convert it to either a DateTime object or a new string format.

function Convert-DateStringToDateTimeObject {
<#
.DESCRIPTION
Parses a datetimestring with a defined pattern to a datetime object
.PARAMETER DateString
Defines the string to parse
.PARAMETER PatternIn
Defines the pattern that datestring is formatted in
.PARAMETER PatternOut
Optional. If this parameter is omitted a standard datetime object is
returned. It is however possible to define an output pattern where
the datetime object is converted back to a string but with the output
pattern instead.
.PARAMETER Culture
Defines to culture to use for conversion. Default is console default ($PSCulture)
.EXAMPLE
$InputString = '2018_06_11_11_05_03'
Convert-DateStringToDateTimeObject -DateString $InputString -PatternIn
'yyyy_MM_dd_HH_mm_ss' -PatternOut 'yyyy-MM-dd HH:mm:ss'
Convert the string date time representation '2018_06_11_11_05_03' to a
valid datetime object and formats that datetime object to a new string format.
.NOTES
AUTHOR Hannes Palmquist
AUTHOREMAIL hannes.palmquist@outlook.com
COPYRIGHT © 2019, Hannes Palmquist, All Rights Reserved
#>
param(
[Parameter(Mandatory)][string]$DateString,
[Parameter(Mandatory)][string]$PatternIn,
[string]$PatternOut = '',
[string]$Culture = $PSCulture
)
$DateTimeFormat = [cultureinfo]::GetCultureInfo($Culture).DateTimeFormat
$DateTimeObject = [DateTime]::ParseExact($DateString, $PatternIn, $DateTimeFormat)
if ($PatternOut -eq '') {
Write-Output $DateTimeObject
} else {
Write-Output $DateTimeObject.ToString($PatternOut)
}
}

So if we have a date/time string like “2018_06_11_11_05_03” we can convert that date time to a date time object by writing:

Convert-DateTimeStringToDateTimeObject -InputString "2018_06_11_11_05_03" -PatternIn "yyyy_MM_dd_HH_mm_ss"
11 juni 2018 11:05:03

You can also use the parameter “PatternOut” to set a specific format to return the DateTime object as.

Note that “\” (backslash) and “:” (semi-colon) needs to be escaped.

Get-WeekInfo

Hannes Palmquist

Hannes Palmquist

Senior Consultant Cloud

In some parts of the world it is more common to work with weeks as measurement of time. Unfortunately there are not easy accessible ways to work with weeks in powershell or .NET. There is some support of retrieving a week number with the culture datatype however if you have a week number and want to resolve dates relatated to that week number you have to resolve that manually. Here is one example of how to do it.

First we define a supporting function that simply retreives the week number for a given date from the Gregorian calendar.

function Get-CalendarWeek {
<#
.DESCRIPTION
Gets the current week number based on a specific culture and it's week number descision rules.
.PARAMETER Date
Defines the date at which to return the week number for. Defaults to the current date.
.PARAMETER CultureInfo
Defines the culture that should be used to calculate the week number. Defaults to se-SV.
.EXAMPLE
Get-CalendarWeek
Get the week number for the current date.
.EXAMPLE
Get-CalendarWeek -Date 2018-01-25 -CultureInfo en-US
Get the week number for the date 2018-01-25 according to the week number calculation rules of the en-US culture.
.NOTES
Author: Hannes Palmquist
AuthorEmail: hannes.palmquist@outlook.com
COPYRIGHT: © 2019, Hannes Palmquist, All Rights Reserved
#>
param(
[datetime]$Date = (Get-Date),
[string]$CultureInfo = $PSCulture
)
# Get specific culture object
$Culture = [cultureinfo]::GetCultureInfo($CultureInfo)
# retrieve calendar week
write-output $Culture.Calendar.GetWeekOfYear($Date, $Culture.DateTimeFormat.CalendarWeekRule, $Culture.DateTimeFormat.FirstDayOfWeek)
}

When we have that function we can define the function that can resolve the week.

function Get-WeekInfo {
<#
.DESCRIPTION
Gets info about a specific week
.PARAMETER Week
Defines the week number to query
.PARAMETER Year
Defines which year to query
.EXAMPLE
Get-WeekInfo -Week 5 -Year 1988
Gets the first date of the fifth week of 1988
.NOTES
Author: Hannes Palmquist
AuthorEmail: hannes.palmquist@outlook.com
Copyright: (c) 2019, Hannes Palmquist, All Rights Reserved
#>
[CmdletBinding()] # Enabled advanced function support
param(
[Parameter(Mandatory)][ValidateRange(1, 53)][int]$Week,
[Parameter(Mandatory)][ValidateRange(1600, 2100)][int]$Year
)
BEGIN {
$WeekHash = [ordered]@{
Week = $Week
Year = $Year
}
}
PROCESS {
$ReferenceDate = Get-Date -Year $Year -Month 02 -Date 05
$ReferenceWeek = Get-CalendarWeek -Date $ReferenceDate
$WeeksDiff = $Week - $ReferenceWeek
$DateInWeek = $ReferenceDate.AddDays($WeeksDiff * 7)
$WeekHash.FirstDateOfWeek = $DateInWeek.AddDays(1 - [int]$DateInWeek.DayOfWeek)
$WeekHash.LastDateOfWeek = $WeekHash.FirstDateOfWeek.AddDays(7).AddMilliseconds(-1)
$WeekHash.StartsInMonth = ([cultureinfo]::GetCultureInfo($PSCulture)).DateTimeFormat.MonthNames[($WeekHash.FirstDateOfWeek).Month-1]
$WeekHash.EndsInMonth = ([cultureinfo]::GetCultureInfo($PSCulture)).DateTimeFormat.MonthNames[($WeekHash.LastDateOfWeek).Month-1]
}
END {
Write-Output ([pscustomobject]$WeekHash)
}
}

This will allow us to perform queries like:

Get-WeekInfo -Week 30 -Year 2018
Week : 30
Year : 2018
FirstDateOfWeek : 2018-07-23 00:00:00
LastDateOfWeek : 2018-07-29 23:59:59
StartsInMonth : July
EndsInMonth : July

Exchange RecipientTypes

Hannes Palmquist

Hannes Palmquist

Senior Consultant Cloud

Below are all current recipient types. Please comment below if you miss an entry in any of the tables.

msExchRecipientDisplayType#

DisplayNameNameValue
ACL able Mailbox UserACLableMailboxUser1073741824
Security Distribution GroupSecurityDistributionGroup1043741833
Equipment MailboxEquipmentMailbox8
Conference Room MailboxConferenceRoomMailbox7
Remote Mail UserRemoteMailUser6
Private Distribution ListPrivateDistributionList5
OrganizationOrganization4
Dynamic Distribution GroupDynamicDistributionGroup3
Public FolderPublicFolder2
Distribution GroupDistrbutionGroup1
Mailbox UserMailboxUser0
Synced Universal Security Group as Universal Security GroupSyncedUSGasUSG-1073739511
ACL able Synced Universal Secuirty Group as ContactACLableSyncedUSGasContact-1073739514
ACL able Synced Remote Mail UserACLableSyncedRemoteMailUser-1073740282
ACL able Synced Mailbox UserACLableSyncedMailboxUser-1073741818
Synced Universal Security Group as ContactSyncedUSGasContact-2147481338
Synced Universal Security Group as Universal Distribution GroupSyncedUSGasUDG-2147481343
Synced Equipment MailboxSyncedEquipmentMailbox-2147481594
Synced Conference Room MailboxSyncedConferenceRoomMailbox-2147481850
Synced Remote Mail UserSyncedRemoteMailUser-2147482106
Synced Dynamic Distribution GroupSyncedDynamicDistributionGroup-2147482874
Synced Public FolderSyncedPublicFolder-2147483130
Synced Universal Distribution Group as ContactSyncedUDGasContact-2147483386
Synced Universal Distribution Group as Universal Distribution GroupSyncedUDGasUDG-2147483391
Synced Mailbox UserSyncedMailboxUser-2147483642

msExchRecipientTypeDetails#

DisplayNameNameValue
Team MailboxTeamMailbox137438953472
Remote Shared MailboxRemoteSharedMailbox34359738368
Remote Equipment MailboxRemoteEquipmentMailbox17179869184
Remote Equipment Mailbox (IncorrectValue)RemoteEquipmentMailbox17173869184
Remote Room MailboxRemoteRoomMailbox8589934592
Remote User Mailbox�����RemoteUserMailbox2147483648
Role GroupRoleGroup1073741824
Discovery MailboxDiscoveryMailbox536870912
Room ListRoomList268435456
Linked UserLinkedUser33554432
Mailbox PlanMailboxPlan16777216
Arbitration MailboxArbitrationMailbox8388608
Microsoft ExchangeMicrosoftExchange4194304
Disabled UserDisabledUser2097152
Non-Universal GroupNonUniversalGroup1048576
Universal Security GroupUniversalSecurityGroup524288
Universal Distribution GroupUniversalDistributionGroup262144
ContactContact131072
UserUser65536
Cross-Forest Mail ContactMailForestContact32768
System MailboxSystemMailbox16384
System Attendant MailboxSystemAttendantMailbox8192
Public FolderPublic Folder4096
Dynamic Distribution GroupDynamicDistributionGroup2048
Mail-Enabled Universal Security GroupMailUniversalSecurityGroup1024
Mail-Enabled Non-Universal Distribution GroupMailNonUniversalGroup512
Mail-Enabled Universal Distribution GroupMailUniversalDistributionGroup256
Mail UserMailUser128
Mail ContactMailContact64
Equipment MailboxEquipmentMailbox32
Room MailboxRoomMailbox16
Legacy MailboxLegacyMailbox8
Shared MailboxSharedMailbox4
Linked MailboxLinkedMailbox2
User MailboxUserMailbox1

msExchRemoteRecipientType#

DisplayNameValue
Migrated, SharedMailbox100
SharedMailbox96
Migrated Equipment Mailbox68
Provisioned Equipment Mailbox65
EquipmentMailbox64
Migrated Room Mailbox36
Provisioned Room Mailbox33
RoomMailbox32
DeprovisionArchive, Migrated User Mailbox20
DeprovisionArchive16
DeprovisionMailbox8
Migrated User Mailbox, ProvisionedArchive (Migrated MBX & Cloud Archive)6
Migrated User Mailbox4
Provisioned User Mailbox, Provisioned User Archive (Cloud MBX & Cloud Archive)3
ProvisionedArchive (Cloud Archive)2
Provisioned User Mailbox (Cloud MBX)1

Setup Active Directory One-way Trust With Selective Authentication

Hannes Palmquist

Hannes Palmquist

Senior Consultant Cloud

One of the things I often help clients with is to setup Active Directory Forest Trusts. If the trust can be setup as “Forest trust”, “Two-way”, “Forest-wide auth”, “conditional forward for DNS”, “no firewall” anyone can manage to setup a forest trust. In reality though it rarely is that simple. Recently I was asked to setup a trust in a more complex scenario.

  • Forest trust
  • One-way
  • Selective Authentication
  • No AD DNS (Third party DNS in both organizations).
    • Not allowed to setup a stub zone or conditional forward
  • And last but not least a very strict security team that was not so keen on opening all port required for the AD trust.

Scenario and definitions#

The scenario came from the acquisition of another company and during the consolidation of the two organization the trust was setup so that the migrated users could access a few legacy systems until all resources was migrated. In the table below the scenario is defined.

PropertyForest AForest B
ContainingUsersResources
Trust DirectionIncomingOutgoing
TrustTrustedTrusting
Direction of Access->
Direction of Trust<-

Configure trust options#

For forest trust authentication there is two options. Selective Authentication and Forest-wide authentication. With forest-wide authentication the trusting forest will allow all authentication requests to authenticate. This means that all users in the trusted forest can authenticate in the trusting forest. This also means that all users in the trusted forest implicitly is added to the “Authenticated Users” group in AD. This is not always desirable. Sometimes common file shares has “authenticated users” as an permission group (which is of course is bad). Authenticated users are also granted some read permissions in Active Directory by default. This means that you can enumerate users and quite a lot or their attributes.

To mitigate this the authentication method “Selective Authentication” exists. This mode will deny all authentication requests by default. This means that for a user from the trusted forest to authenticate to a resource, that user needs to be granted the “Allowed to authenticate” permission on the resources active directory computer object. This allows us to control which users can authenticate, to which resource in addition to the standard permission for the resource itself.

In this scenario “Selective Authentication” is selected.

Configure network port openings#

Most likely there is a firewall between the two organizations and active directory by nature uses a lot of different ports for different type of communication. There are many blogs, articles that try to summarize whats ports are needed for a forest trust but all of them manage to include a lot of ports that are often not needed or for specific scenarios or types of trusts. In our scenario I didn’t want to order port openings that I couldn’t motivate that we needed in this specific case. Below is a list of the ports that I ended up with a bare minimum to setup the a working trust according to our requirements.

important

Note that even though the trust is one-way does not mean that the communication is. These ports needs to be opened from domain controllers in ForestA to domain controllers in ForestB and vice versa

PortProtocolServiceDescriptionType
88TCPKerberosUsed for DC AuthMandatory
88UDPKerberosUsed for DC AuthMandatory
135TCPRPC Endpoint MapperUsed to establish a RPC endpointMandatory
389TCPLDAPUsed for LDAP commMandatory
389UDPLDAPUsed for LDAP commMandatory
455TCPSMBUsed for trust establishment. Can be removed post trust configuration.Mandatory during setup
1024-65535TCPRPCRPC High ports returned by RPC Endpoint mapperMandatory

Configure DNS records#

Often DNS can be configured by just setting up a AD DNS conditional forward for the other forest. This will allow all necessary DNS records to be resolved by the respective forest. In this scenario though we were not allowed to do that and instead create the DNS records manually. Almost no documentation exists for this scenario so here comes the least amount of DNS records required to successfully set up the forest trust.

A-records#

NameTarget
domain.comDC1-IP
domain.comDC2-IP
domain.comDC3-IP
DC1.domain.comDC1-IP
DC2.domain.comDC2-IP
DC3.domain.comDC3-IP

Notes

  • All domain controllers that should serve the trust needs to be added as A records.
  • Make sure that the KDC and PDC are among these domain controllers.

SRV-records#

  • Subdomain “_msdcs” of “domain.com” needs to be created
  • Subdomain “dc” of domain “_msdcs.domain.com” needs to be created
  • Subdomain “pdc” of domain “_msdcs.domain.com” needs to be created if both sides of the forest trust should be created from one of the sides.
  • Note the trailing “.” (period) of the host names
Full nameServiceProtocolPortPriorityWeightHost
_ldap._tcp.dc_msdcs.domain.com_ldap_tcp3890100DC1.domain.com
_ldap._tcp.dc_msdcs.domain.com_ldap_tcp3890100DC2.domain.com
_ldap._tcp.dc_msdcs.domain.com_ldap_tcp3890100DC3.domain.com
_kerberos._tcp.dc._msdcs.domain.com_kerberos_tcp880100DC1.domain.com
_kerberos._tcp.dc._msdcs.domain.com_kerberos_tcp880100DC2.domain.com
_kerberos._tcp.dc._msdcs.domain.com_kerberos_tcp880100DC3.domain.com
_ldap._tcp.pdc._msdcs.domain.com_ldap_tcp3890100DC1.domain.com(PDC domain controller)

If conditional forwards are used, make sure that all domain controllers that are resolvable are added with port openings and that they are reachable.

Other notes#

  • Make sure that all clients where users from ForestA can reach the domain controllers published in DNS for ForestB.
  • Use PortQry to test all ports from all domain controllers.
    • Note that 88:UDP don’t give any response so that port cannot be tested.
  • This guide focus on establishing the trust between two forests. The following topics are out-of-scope and subject for a future post.
    • Port openings for client computers and resource services.
    • Configuration of permissions to authenticate through the trust.
    • Configuration of permissions to access resources in ForestB.

Change desktop wallpaper with powershell

Hannes Palmquist

Hannes Palmquist

Senior Consultant Cloud

I’ve seen numerous forums and blog articles trying to to change desktop wallpaper in windows, none of which works reliably. The most common solution is to set a new registry keys and then call user32.dll and the method UpdatePerUserSystemParameters and then quite literally hope that the desktop wallpaper changes. This is not always the case because Windows does not always honor the request to actually update the wallpaper settings when this method is called. The inner working of this method is not completely known and this method has never been advertised by Microsoft to be the way to change wallpaper.

However I came to the conclusion that it must exist a documented windows API to actually set a new wallpaper so I started looking into C# solutions to the same problem and sure thing it was a quite an easy procedure to change the desktop wallpaper. All I had to do was to define the type definition in Powershell and then pass the action values when calling the SystemParametersInfo method.

The below Powershell function will reliably change the desktop wallpaper and you also have the possibility to choose the style.

<#PSScriptInfo
.VERSION 1.0.0.0
.GUID cfc2e719-67d8-4722-b594-3d198a1206c7
.FILENAME Set-DesktopWallpaper.ps1
.AUTHOR Hannes Palmquist
.AUTHOREMAIL hannes.palmquist@outlook.com
.CREATEDDATE 2019-10-14
.COMPANYNAME Personal
.COPYRIGHT (c) 2019, Hannes Palmquist, All Rights Reserved
#>
function Set-DesktopWallpaper {
<#
.DESCRIPTION
Sets a desktop background image
.PARAMETER PicturePath
Defines the path to the picture to use for background
.PARAMETER Style
Defines the style of the wallpaper. Valid values are, Tiled, Centered, Stretched, Fill, Fit, Span
.EXAMPLE
Set-DesktopWallpaper -PicturePath "C:\pictures\picture1.jpg" -Style Fill
.EXAMPLE
Set-DesktopWallpaper -PicturePath "C:\pictures\picture2.png" -Style Centered
.NOTES
Author: Hannes Palmquist
AuthorEmail: hannes.palmquist@outlook.com
COPYRIGHT: © 2019, Hannes Palmquist, All Rights Reserved
Supports jpg, png and bmp files.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)][string]$PicturePath,
[ValidateSet('Tiled', 'Centered', 'Stretched', 'Fill', 'Fit', 'Span')]$Style = 'Fill'
)
BEGIN {
$Definition = @"
[DllImport("user32.dll", EntryPoint = "SystemParametersInfo")]
public static extern int SystemParametersInfo(int uAction, int uParam, string lpvParam, int fuWinIni);
"@
Add-Type -MemberDefinition $Definition -Name Win32SystemParametersInfo -Namespace Win32Functions
$Action_SetDeskWallpaper = [int]20
$Action_UpdateIniFile = [int]0x01
$Action_SendWinIniChangeEvent = [int]0x02
$HT_WallPaperStyle = @{
'Tiles' = 0
'Centered' = 0
'Stretched' = 2
'Fill' = 10
'Fit' = 6
'Span' = 22
}
$HT_TileWallPaper = @{
'Tiles' = 1
'Centered' = 0
'Stretched' = 0
'Fill' = 0
'Fit' = 0
'Span' = 0
}
}
PROCESS {
Set-ItemProperty -Path 'HKCU:\Control Panel\Desktop' -Name wallpaperstyle -Value $HT_WallPaperStyle[$Style]
Set-ItemProperty -Path 'HKCU:\Control Panel\Desktop' -Name wallpaperstyle -Value $HT_TileWallPaper[$Style]
$null = [Win32Functions.Win32SystemParametersInfo]::SystemParametersInfo($Action_SetDeskWallpaper, 0, $PicturePath, ($Action_UpdateIniFile -bor $Action_SendWinIniChangeEvent))
}
END {
}
}

How to verify group membership

Hannes Palmquist

Hannes Palmquist

Senior Consultant Cloud

This function can be used to show the status of the Powershell profile scripts on the computer.

function Check-ProfileStatus {
($profile | Get-Member -MemberType NoteProperty).Name |
ForEach-Object {
$CurrentProfile = $_
$path = $profile.$_
[pscustomobject]([Ordered]@{Profile=$CurrentProfile;Path=$Path;Exists=(Test-Path $Path)})
}
}
Check-ProfileStatus

Check-ProfileStatus

Hannes Palmquist

Hannes Palmquist

Senior Consultant Cloud

This function can be used to show the status of the Powershell profile scripts on the computer.

function Check-ProfileStatus {
($profile | Get-Member -MemberType NoteProperty).Name |
ForEach-Object {
$CurrentProfile = $_
$path = $profile.$_
[pscustomobject]([Ordered]@{Profile=$CurrentProfile;Path=$Path;Exists=(Test-Path $Path)})
}
}
Check-ProfileStatus