Skip to main content

· 3 min read
Hannes Palmquist

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

· 4 min read
Hannes Palmquist

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

· 6 min read
Hannes Palmquist

One of the things I have helped 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 strict security policies for firewall rules

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 are 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 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 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
445TCPSMBUsed 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.

· 2 min read
Hannes Palmquist

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
#>
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
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 tilewallpaper -Value $HT_TileWallPaper[$Style]
$null = [Win32Functions.Win32SystemParametersInfo]::SystemParametersInfo($Action_SetDeskWallpaper, 0, $PicturePath, ($Action_UpdateIniFile -bor $Action_SendWinIniChangeEvent))
}
}

· One min read
Hannes Palmquist

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

· One min read
Hannes Palmquist

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

· 3 min read
Hannes Palmquist

One thing that I have been struggling with from time to time is that the cmdlet Where-Object is incredibly slow to filter massive datasets. Lets say you have custom PSObject array with 50000 objects and 20 properties each. If you would cross referencing this table with another large dataset using the Where-Object cmdlet for each lookup it would take ages.

One day I had to do such a comparison and I was forced to come up with an alternate way of retrieving matches, so I developed a new function that is much faster than the Where-Object cmdlet.

Lets say you have a CSV-file containing 50000 rows and 20 columns with one column being a GUID. First you need to create an index:

$CSVIndex = $CSV.GUID.ToLower()

Once that is done the search can be started using the cmdlet below:

Fast-Search -Database $CSV -DatabaseIndex $CSVIndex -SearchString "A52FB-...-27422"

This is how the function is defined

function Fast-Search {
param(
$Database,
$DatabaseIndex,
$SearchString
)

$Array = @()
$Index = 0

while ($Index -ne -1) {
$Index = system.array]::IndexOf($DatabaseIndex,$SearchString,$Index)
if ($Index -ne -1) {
$Array += $Index
$Index++
}
}
$Array | ForEach-Object {
$Database[$_]
}
}

What makes the function so much faster you might ask..

First of, the key is that the dataset and the dataset index does not change order internally in the array as we assume that the item on Index=X is the same item both in the dataset and the dataset index.

So what we do is to search for the SearchString only in the dataset index, this in itself i much faster as it does not have to process as much data. Then we use the method IndexOf of the dataset index. This is also quite fast localizing the first row that matches the SearchString. Then we save that index in another array, lets call it the result array. When that is done we continue to search for the next match after the last result. This process is repeated until we reach the end of the dataset index.

We then have an array of indexes with the “index numbers” of the rows that match the SearchString. The Last thing we need to do is to collect the rows from the large dataset using array index targeting.

$SomeArray[$TheIndexThatWeWantToRetreive]

And last but not least, we return all the objects from the dataset.

In some cases I have had performance benefits by using this method by up to 80 times compared to using Where-Object. The drawback is that it isn’t a built-in cmdlet so you have to declare the function and also you need to build an index manually and last that you can only search in one property at a time, the index that you created. You should only use this method for the specific use cases when you have two very large datasets where the key isn’t unique. The function can also be developed further to accept two or more indexes in case you need to search for more than one property.

A similar solution is to use a hashtable as dataset index lookup table and simply store the index value as key and the whole object as the value of the key. This method is quite easy to use however it has one drawback; keys must be unique. So if you need to search a large dataset fast where you expect more than one result based on the index this function give you really fast searches.