PowerShell

Check whether a port is open on a bulk list of endpoints – and FAST!

Hooray and welcome to 2023! May all of your dreams come true this year, and if not, may we at least be sheltered from P1 outages and other disasters, either natural or man-made…!

🤞🙏🤞🙏🤞🙏🤞🙏🤞🙏🤞🙏🤞🙏🤞🙏🤞🙏

Recently I needed to see if RDP was open on hundreds of machines. No trouble I thought, and wrote a wee script that looped through a list of names or IPs using Test-NetConnection with the -Port switch.

This worked no problem, BUT… Test-NetConnection can take 15+ seconds per machine (especially when waiting to ‘time out’ if the port is closed). Not good enough, we want it to be faster!

Unfortunately, Test-NetConnection does not currently support any switches to control how long it waits for a response, although I’m sure they will add it at some point.

So, a Googling we go then! And again, I am reminded of my average intelligence when it comes to PowerShell. But that’s okay – learning all the time is a good thing, right? 😜

I found this little beowtae of a function called Test-PortScan which uses the TcpClient object to do the job faster. The original is found here. Thanks to the author ‘BSonPosh’. 👌

As I created this script I also thought “I’d like any functions to be at the bottom of the script, so the main editable code is at the top”. Turns out this is quite easy to do by storing the main code in a variable, then calling it after the functions. Full script below; you’ll want to edit the variables at the top – Name of the service or test, the port number, the timeout value (I went with half a second) and change the import and logfile locations if required. Cheers. Simon! 🤟🙂🤟

# uses test-portscan function to quickly check whether a port is open on multiple IP4 endpoints

$mainblock = {
Clear-Host

# set variables
$logfile = 'c:\temp\portcheck_log.txt'
$servicename = 'HTTP'
$portnumber = '80'
$timeoutmilliseconds = '500'

# import single column of endpoints from a file (no header required)
$endpoints = Get-Content C:\Temp\endpoints.txt -Force

# or use an array
# $endpoints = @(
# "192.168.1.254"
# "1.1.1.1"
# "8.8.8.8"
# "www.microsoft.com"
# "www.bleepingcomputer.com"
# )

# create log file
$null = New-Item -ItemType File -Path $logfile -Force

# perform the checks
foreach ( $endpoint in $endpoints ) {

    Write-Host "Testing $testname access to $endpoint..."

    if ((Test-PortScan -Devices $endpoint -StartPort $portnumber -EndPort $portnumber -Timeout $timeoutmilliseconds).Open -ne 'False') {
       Write-Host -ForegroundColor Gray "$testname is not open on $endpoint."
    }
        else {
            Write-Host -ForegroundColor Green "$testname is open on $endpoint! - adding to $logfile"
            $output = "$testname is open on $endpoint."
            $output | Out-File $logfile -Append -Force
        }
}

# end of mainblock
}

# port checking function
function Test-PortScan
{   
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true,
                   ValueFromPipeline = $true,
                   ValueFromPipelineByPropertyName = $true,
                   Position = 0,
                   HelpMessage = 'You must supply at least a device name or IP')]
        [Alias('Name')]
        [string]$Devices,
        [Parameter(Position = 1)]
        [int]$StartPort = 1,
        [Parameter(Position = 2)]
        [int]$EndPort = $StartPort,
        [Parameter(Position = 3)]
        [int]$Timeout = 100
    )

    BEGIN
    {
        $PSObject = @()
        $Output = @()
    }
    PROCESS
    {
        foreach ($Device in $Devices)
        {
            Write-Verbose "Not to the port loop yet"
            $Port = $StartPort
            do
            {
                #($Port = $StartPort; $Port -gt $EndPort; $Port++)              
                Write-Verbose "Made it to the port loop"
                $requestCallback = $state = $null
                $client = New-Object System.Net.Sockets.TcpClient
                $beginConnect = $client.BeginConnect($Device, $Port, $requestCallback, $state)
                Start-Sleep -Milliseconds $Timeout
                if ($client.Connected) { $Open = $true }
                else { $Open = $false }
                $client.Close()
                [pscustomobject]@{
                    'Computer' = $Device
                    'Port'     = $Port
                    'Open'     = $Open
                }
                $Port += 1
            }
            until ($Port -gt $EndPort)
        }
        
    }
    END
    {
        
    }
}

# run mainblock
& $mainblock

# end

Loading

Using Forms with PowerShell – ITs awesome!

Using forms in PowerShell allows you to run interactive scripts that provide information or prompt for input. Ever need to force everyone to reboot? I know, I know… why on earth would you spend time on that Simon? Well (a) I’m absolutely bonkers… and (b) it’s fun! –ahem, refer to (a).

The main problem with using forms with PowerShell is that the script needs to run in the logged on users context, which prevents us performing any admin tasks. Depending on the requirements you may also need to deploy some PowerShell modules out to the workstations… uuurg that is already sounding OTT!

Well, for that I’ll share another post soon where I took a ‘queue’ approach and split the code to have a workstation-side script that runs in user context, and a server-side script that runs under a service account (assigned whichever admin permissions are required for its tasks). The workstation script gathers required data using forms etc., then writes a csv file into a shared folder on the server. The server script is checking the folder, sees a new file and processes it. Once a successful result is confirmed, the file is deleted. Meanwhile the workstation script has been in a ‘Please wait’ state, checking for the file deletion so it can continue knowing a server-side task is complete. “Coolio Glesias” I hear you say!

Anyway, that’s a longer post for another time! 🙂🙂🙂

This script below will prompt the user to reboot their workstation as soon as possible, with a mandatory reboot countdown visible. The form cannot be closed (unless the user can kill the PowerShell process – block that with a GPO that disables Task Manager and command line access). If the countdown reaches zero, a reboot is forced.

You can use GPO to deploy the script to a folder on each workstation, and also to configure a Scheduled Task for running it. Or in my case I am importing it into an MSP platform so it can be run automatically or on demand when a reboot is pending on client computers.

Aside from what it does (which is nothing fantastic!), it’s good example of using forms in PowerShell. I’ll post about my cross-domain password sync script soon which adds different types of message box as functions. Until then, Hei Konei Ra and have a Merry Xmas!

🧑‍🎄🍻🧑‍🎄🍻🧑‍🎄

# displays a non-dismissable warning with countdown in top left of screen
# force the local machine to reboot after $timeinseconds e.g. 28800 seconds = 8 hours.

# update variables as required
# default 8 hour setting
$timeinseconds = 28800

# icon for top-left of forms
$formicon = "$env:SystemRoot\UUS\amd64\WindowsUpdateWarning.ico"

# load the stuff we need for the forms
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing

# start the countdown

$Form = New-Object system.Windows.Forms.Form
$Form.Text = "SBEnterprises - Reboot Required"
$Form.Location = New-Object System.Drawing.Point(15,10)
$Form.AutoSize = $true # .Size = New-Object System.Drawing.Size(350,250)
$Form.AutoSizeMode = 'GrowAndShrink'
$Form.ShowInTaskbar = $false
$Form.Icon = New-Object System.Drawing.Icon "$formicon"
$Form.ShowIcon = $true
$Form.Font = New-Object System.Drawing.Font("Segoe UI",10)
$Label = New-Object System.Windows.Forms.Label
$Label.Location = New-Object System.Drawing.Point(5,10)
$Label.AutoSize = $true # .Size = New-Object System.Drawing.Size(350,250)
$Label.TextAlign = 'MiddleCenter'
$Label.UseWaitCursor = $false
$Form.Controls.Add($Label)

While ($timeinseconds -gt 0) {

    $timespan = New-TimeSpan -Seconds $timeinseconds

    $hour = $timespan.Hours
    $min = $timespan.Minutes
    $sec = $timespan.Seconds

    $Form.Show()
    $Label.Text = "WARNING!`nYour workstation requires a reboot to finish installing updates...`nPlease reboot as soon as possible.`n`nA mandatory reboot will occur in $hour hours, $min minutes, $sec seconds.`n`n"
     
    Start-Sleep -Seconds 1
    $timeinseconds = $timeinseconds - 1 
}

$Form.Close()
Restart-Computer -Force

# end

Loading

Azure AD – export groups and members #2 – advanced version!

Due to the popularity of the initial script (over 5000 views and 3rd in the list on a google search – cheers!) Azure AD – Export Groups and Members to CSV, and thanks to David for asking, this script goes next level and will export the groups and the members with properties ObjectID, Display Name, UserPrincipalName and Email Address. It caters for the main member ‘types’, User, Device, Group, and Contact. If another type of object is a member the output will say ‘Unknown object’. This could be a service principal or other object which you can investigate using the ObjectId. If a group has no members, ‘No members’ is output as the member display name.

The script uses the AzAD cmdlets as well as the AzureAD cmdlets, so make sure you have installed and imported them.

To install them:

Install-Module Az -SkipPublisherCheck -Force -AllowClobber -Confirm:$false

Install-Module AzureAD -SkipPublisherCheck -Force -AllowClobber -Confirm:$false

To import them:

Import-Module Az

Import-Module AzureAD

Enjoy! 🍻🤟🙂🤟🍻

$allgroups = Get-AzADGroup

$result = foreach ( $group in $allgroups ) {

    $hash = @{
        GroupName=$group.DisplayName
        Member=''
        Email=''
        UserPrincipalName=''
        ObjectId=''
    }
    
    $groupid = $group.id
    $groupdisplayname = $group.DisplayName

        if ( $members = Get-AzADGroupMember -GroupObjectId $groupid ) {

            foreach ( $member in $members ) {

                if ( $member.OdataType -eq '#microsoft.graph.user' ) {

                    $objectid = $member.Id
                    $userinfo = Get-AzADUser -ObjectId $objectid
                    $displayname = $userinfo.DisplayName
                    $email = $userinfo.Mail
                    $upn = $userinfo.UserPrincipalName
                     
                    $hash.Member = $displayname
                    $hash.Email = $email
                    $hash.UserPrincipalName = $upn
                    $hash.ObjectId = $objectid
                    New-Object psObject -Property $hash
                }

                elseif ( $member.OdataType -eq '#microsoft.graph.group' ) {

                    $objectid = $member.Id
                    $userinfo = Get-AzADGroup -ObjectId $objectid
                    $displayname = $userinfo.DisplayName
                    $email = $userinfo.Mail
                    $upn = 'No UPN - Nested Group'
                     
                    $hash.Member = $displayname
                    $hash.Email = $email
                    $hash.UserPrincipalName = $upn
                    $hash.ObjectId = $objectid
                    New-Object psObject -Property $hash                
                }
                
                elseif ( $member.OdataType -eq '#microsoft.graph.orgContact' ) {

                    $objectid = $member.Id
                    $userinfo = Get-AzureADContact -ObjectId $objectid
                    $displayname = $userinfo.DisplayName
                    $email = $userinfo.Mail
                    $upn = 'No UPN - Contact'
                     
                    $hash.Member = $displayname
                    $hash.Email = $email
                    $hash.UserPrincipalName = $upn
                    $hash.ObjectId = $objectid
                    New-Object psObject -Property $hash
                }

                elseif ( $member.OdataType -eq '#microsoft.graph.device' ) {

                    $objectid = $member.Id
                    $userinfo = Get-AzureADDevice -ObjectId $objectid
                    $displayname = $userinfo.DisplayName
                    $email = 'No Email - Device'
                    $upn = 'No UPN - Device'
                     
                    $hash.Member = $displayname
                    $hash.Email = $email
                    $hash.UserPrincipalName = $upn
                    $hash.ObjectId = $objectid
                    New-Object psObject -Property $hash
                
                }

                else {
                    $objectid = $member.Id
                    $displayname = 'Unknown object'
                    $email = 'Unknown object'
                    $upn = 'Unknown object'
                     
                    $hash.Member = $displayname
                    $hash.Email = $email
                    $hash.UserPrincipalName = $upn
                    $hash.ObjectId = $objectid
                    New-Object psObject -Property $hash

                }
            }
        }

        else {
           $hash.Member = 'No members'
           $hash.Email = ''
           $hash.UserPrincipalName = ''
           $hash.ObjectId = ''
           New-Object psObject -Property $hash

        }
}

$result | Export-Csv -Path c:\temp\aadgroupsandmembers.csv -NoTypeInformation

Loading

GAL separation with Address Book Policies

Just a quick one on ABPs – there are many posts about this topic now, but there are few that mention how to resolve issues such as recipients not appearing in the list when they should be. In this case you need to ‘tickle’ (yes that’s the official Microsoft term) the objects to get them to play ball.

ABPs seemed complex when I first looked at them, and my first introduction was with a tenant that had 20,000 user objects! We don’t get many opportunities to work with environments of this size in New Zealand, so it was a great job to get involved with. #dontmessitupmaaate!

ABPs are most commonly used in large environments though, or where separation is needed. Examples of this are:

  • Multiple schools under one tenant – you don’t want students from one school seeing students from other schools in the Global Address List.
  • Multiple companies under one tenant. You don’t want Fabrikam users seeing Contoso users.
  • In both of these scenarios, you may want management or executive level staff to see all recipients in their GAL.

Here is the code to create each ABP… I’m using my fictional company SB Enterprises which exists in a large multi-company tenant. Customattribute1 is used across the tenant to identify the objects related to a particular company with a three-letter-acronym. (this could be the Company attribute, or any other attribute as long as we can use it for filtering in the commands). For Meeting Room, Equipment, or Contact objects, those are created with the company TLA at the front e.g. SBE_MeetingRoom1.

Firstly let’s connect to Exchange Online powershell:

Connect-ExchangeOnline -UserPrincipalName insertadminupnhere

Now let’s create some address lists to include in our policy:

# this one contains all our recipients (users, groups, and shared mailboxes)
New-AddressList -Name "SBE_AddressList" -RecipientFilter "(CustomAttribute1 -eq 'SBE')"

# this one contains our groups
New-AddressList -Name "SBE_Groups" -RecipientFilter "(ObjectClass -like 'group') -and (CustomAttribute1 -eq 'SBE')"

# this one contains our shared mailboxes
New-AddressList -Name "SBE_Shared Mailboxes" -RecipientFilter "(RecipientTypeDetails -eq 'SharedMailbox') -and (CustomAttribute1 -eq 'SBE')"

# this one contains our rooms
New-AddressList -Name "SBE_Rooms" -RecipientFilter "(RecipientTypeDetails -eq 'RoomMailbox') -and (Name -like 'SBE_*')"

# this one contains our contacts
New-AddressList -Name "SBE_Contacts" -RecipientFilter "(RecipientType -eq 'MailContact') -and (Name -like 'SBE_*')"

Sweet as lemon pie! Now let’s get a list of all the objects that should be included in each list:


$filter = (Get-AddressList "SBE_AddressList").recipientfilter
Get-Recipient -ResultSize unlimited -RecipientPreviewFilter $filter | Out-GridView

$filter = (Get-AddressList "SBE_Groups").recipientfilter
Get-Recipient -ResultSize unlimited -RecipientPreviewFilter $filter | Out-GridView

$filter = (Get-AddressList "SBE_Shared Mailboxes").recipientfilter
Get-Recipient -ResultSize unlimited -RecipientPreviewFilter $filter | Out-GridView

$filter = (Get-AddressList "SBE_Rooms").recipientfilter
Get-Recipient -ResultSize unlimited -RecipientPreviewFilter $filter | Out-GridView

$filter = (Get-AddressList "SBE_Contacts").recipientfilter
Get-Recipient -ResultSize unlimited -RecipientPreviewFilter $filter | Out-GridView

Looking good? Now let’s create the Global Address List and the Offline Global Address List:

# the global address list has it's own filter for all SBE objects
New-GlobalAddressList -Name "SBE_GlobalAddressList" -RecipientFilter "(CustomAttribute1 -eq 'SBE') -or (Name -like 'SBE_*')"

# the offline address list includes the address lists we created
New-OfflineAddressBook -Name "SBE_OfflineAddressList" -AddressLists "SBE_AddressList","SBE_Groups","SBE_Shared Mailboxes","SBE_Rooms","SBE_Contacts"

Great! Now we can create the Address Book Policy using all of the above:

New-AddressBookPolicy -Name "SBE_ABP" -AddressLists "SBE_AddressList","SBE_Groups","SBE_Shared Mailboxes","SBE_Contacts" -OfflineAddressBook "\SBE_OfflineAddressList" -GlobalAddressList "\SBE_GlobalAddressList" -RoomList "\SBE_Rooms"

Done! Well almost – we have to assign it to the users… I recommend assigning to a pilot group first to get some feedback in case of any issues. When ready, use this command to assign the ABP to all applicable users:

$SBE_ABP = Get-Mailbox -ResultSize unlimited -Filter "(RecipientTypeDetails -eq 'UserMailbox') -and (CustomAttribute1 -eq 'SBE')"; $SBE_ABP | foreach {Set-Mailbox -Identity $_.Identity -AddressBookPolicy 'SBE_ABP'}

Now, there are several things that can make it seem like things are not working, but I can tell you 99% of the time you just have to wait. It’s the old ‘cloud time phenomenon’ where things may take from 1 to 48 hours to take effect 🤣🤣🤣.

The most common issue I have come across is someone in the pilot group pointing out that someone or something is missing from the GAL. This is due to the object not being processed when the Address List was created. It should be a member of the filter; and it is upon object creation or update that membership of address list filters is determined.

This is where ‘tickling’ comes in. You need to change something i.e. any attribute of the offending object, then change it back again.

You can do this in the portal for a single object (e.g. change the last name one letter, save, then change it back again)… but seeing as I know this problem exists, I now do this as part of the initial setup so I don’t have to deal with it later.

Let’s run this to change an attribute – I’ve checked all objects Customattribute5 is blank, so I can use it for this purpose (you don’t have to use tickle obviously, any value will do):


# get the users we want to 'tickle'
$users = Get-User -ResultSize unlimited -Filter "Customattribute1 -eq 'SBE'"

# tickle them by modifying a value (make sure the value was null for all objects beforehand)
foreach ($user in $users) {

    $id = $user.DistinguishedName
    Set-Mailbox $id -CustomAttribute5 "tickled"
}

# then return the value to null
foreach ($user in $users) {

    $id = $user.DistinguishedName
    Set-Mailbox $id -CustomAttribute5 $null
}

And voila, the object now appears on the list (taking into account the ‘cloud time phenomenon’ mentioned above).

As always, thanks for coming dudes & dudettes! ✌🍻✌

Loading

Microsoft 365 cross-tenant migration

Hello! Long time no… 🍻😊🍻

I thought I would share some PowerShell I used during a recent cross-tenant migration. Firstly, the Microsoft documentation is really good and got the journey off to a good start:

https://docs.microsoft.com/en-us/microsoft-365/enterprise/cross-tenant-mailbox-migration?view=o365-worldwide

PLEASE NOTE: Following is some fairly raw PowerShell. You can’t just press Go! You’ll need to understand and update the bits required and run accordingly! Hopefully everything that requires updating is in bold-italics. If not please punish me via comment! =)

That said… let’s continue 😃:

Firstly, let’s create a mail-enabled security group in the source tenant Exchange Admin console, ‘zzMigUsers@sourcedomain.com’ should do the trick! ..then add the users (mailboxes) you will migrate (the migration endpoint is scoped to a group, anyone not in the group will fail to migrate).

Now load PowerShell ISE and paste all of the below code bits in, then save it for later. First, we need our commands to be able to switch quickly between tenants (disconnect before connecting each time):

# Source tenant
Connect-ExchangeOnline -UserPrincipalName migadmin@sourcedomain.com
# Target tenant
Connect-ExchangeOnline -UserPrincipalName migadmin@targetdomain.com
# Disconnect from tenant
Disconnect-ExchangeOnline -Confirm:$false

Now we can connect to the target tenant and create the Org Relationship and Migration Endpoint – you’ll need the ‘sourcedomain‘, app ID and secret to paste in here where the bold italics are (follow the MS article to set up the App Registration and Enterprise App, easy as!): Target tenant app setup.

# connect to target tenant here
# Enable customization if tenant is dehydrated
  $dehy = Get-OrganizationConfig | fl isdehydrated
  if ($dehy -eq $true) {Enable-OrganizationCustomization}

# Create Migration Endpoint in target tenant
$AppId = "paste the app id here"
$Credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $AppId, (ConvertTo-SecureString -String "paste the app secret here" -AsPlainText -Force)
New-MigrationEndpoint -RemoteServer outlook.office.com -RemoteTenant "sourcedomain.onmicrosoft.com" -Credentials $Credential -ExchangeRemoteMove:$true -Name "SourceDomainMigEndpoint" -ApplicationId $AppId

# Create Org Relationship in target tenant
$sourceTenantId="paste source tenant id here"
$orgrels=Get-OrganizationRelationship
$existingOrgRel = $orgrels | ?{$_.DomainNames -like $sourceTenantId}
If ($null -ne $existingOrgRel)
{
    Set-OrganizationRelationship $existingOrgRel.Name -Enabled:$true -MailboxMoveEnabled:$true -MailboxMoveCapability Inbound
}
If ($null -eq $existingOrgRel)
{
    New-OrganizationRelationship "SourceDomainOrgRel" -Enabled:$true -MailboxMoveEnabled:$true -MailboxMoveCapability Inbound -DomainNames $sourceTenantId
}

Let’s use our disconnect / connect commands above to disconnect from the target tenant and connect to the source tenant to set up the other side:

# connect to source tenant here

# Configure OrgRel in source tenant
$targetTenantId="insert the target tenant ID here"
$appId="insert the app id here"
$scope="zzMigUsers@sourcedomain.com"
$orgrels=Get-OrganizationRelationship
$existingOrgRel = $orgrels | ?{$_.DomainNames -like $targetTenantId}
If ($null -ne $existingOrgRel)
{
    Set-OrganizationRelationship $existingOrgRel.Name -Enabled:$true -MailboxMoveEnabled:$true -MailboxMoveCapability RemoteOutbound -OAuthApplicationId $appId -MailboxMovePublishedScopes $scope
}
If ($null -eq $existingOrgRel)
{
    New-OrganizationRelationship "targetdomainOrgRel" -Enabled:$true -MailboxMoveEnabled:$true -MailboxMoveCapability RemoteOutbound -DomainNames $targetTenantId -OAuthApplicationId $appId -MailboxMovePublishedScopes $scope
}

Hopefully you got to this point without issue – if not let me know in the comments section and I’ll try to help! Next, we should be getting a successful test of what we have set up. Run this command since you are still connected to the source tenant – you should see the expected values returned:

# Confirm OrgRel in Source tenant
Get-OrganizationRelationship | fl name, DomainNames, MailboxMoveEnabled, MailboxMoveCapability

Now, disconnect and connect to the target tenant again, and run this:

# Confirm OrgRel and MigEndpoint in Target tenant
Get-MigrationEndpoint
Get-OrganizationRelationship | fl name, DomainNames, MailboxMoveEnabled, MailboxMoveCapability
Test-MigrationServerAvailability -Endpoint "SourceDomainMigEndpoint"

Awesome, green lights!? The tenants are configured. Now disconnect / connect to the source tenant, we’ll need to get the following details out into a CSV file so we can create the MailUser objects in the target tenant (yes I know we could have done that earlier =)). Limit this if required based on the group we created earlier, scoped to your migration users.

# get source mailbox details
Get-Mailbox | select DisplayName, UserPrincipalName, PrimarySMTPAddress, ExchangeGUID, ArchiveGUID, LegacyExchangeDN | Export-Csv C:\temp\sourceusers.csv -NoTypeInformation

Disconnect / connect to the target tenant again. We want to test one user first, so I used these commands (replace bold italic with values for one user from the CSV output):

# create MailUsers in target tenant - ONE AT A TIME HERE OR BELOW FOR BATCHES
$originalalias = 'sales'
$newalias = 'sourcedomain.sales'
$userdisplayname = 'sourcedomain Sales Department'
$exchguid = '80ddefd4-26cb-7621-c497-g6c044b04dd9'
$archguid = '70edegc5-36f8-8639-b287-f6g035408ec2'
$x500address = 'x500:/o=ExchangeLabs/ou=Exchange Administrative Group (FYDIBOHF23SPDLT)/cn=Recipients/cn=ce04185c5ff841f885d660e46e6c8bc5-sourcedomain SAL'
$usersourceaddress = $originalalias + "@sourcedomain.onmicrosoft.com"
$usertargetaddress = $newalias + "@targetdomain.com"
$usertargettenantaddress = $newalias + "@targetdomain.onmicrosoft.com"
New-MailUser -Name $newalias -DisplayName $userdisplayname -ExternalEmailAddress $usersourceaddress -MicrosoftOnlineServicesID $usertargetaddress -Password (ConvertTo-SecureString -String 'Hellosourcedomain2099' -AsPlainText -Force)
Set-MailUser $usertargetaddress -ExchangeGuid $exchguid -ArchiveGuid $archguid -PrimarySmtpAddress $usertargetaddress
Set-MailUser $usertargetaddress -EmailAddresses @{Add="$x500address","$usertargettenantaddress"}
Set-MailUser $usertargetaddress -EmailAddresses @{Remove="smtp:$usersourceaddress"}

Then we can migrate the user:

New-MigrationBatch -Name testbatch -SourceEndpoint 'sourcedomainMigEndpoint' -UserIds $usertargetaddress -Autostart -TargetDeliveryDomain 'targetdomain.onmicrosoft.com'

NOTE: Recently I found this command was not working and I had to use a one-liner copy of the batch1-users.csv mentioned below. After the test user is successfully migrated, reference your original file with all the mailboxes in it. Just in case you get this issue in your scenario 😃

Okay! When this is working and we are confident all is well, copy the sourceusers.csv to a new file named batch1-detail.csv. Open the file and add three columns before DisplayName as below. ‘originalalias’ (the bit before the UPN), ‘newalias’ (set as needed for the target tenant) and ‘newupn’ (new UPN in the target tenant):

Assuming the file is called batch1-detail.csv, create another CSV called batch1-users.csv. This is the input for the migration batch command (very simple and worked well for me), just copy the target UPNs from batch1-detail.csv into the EmailAddress column and populate the other columns as below:

Now you can use this code to create the MailUser objects with the batch1-detail.csv file:

# TO DO BATCHES
# UPDATE THESE CORRECTLY - set variables and import csv
$csv = Import-Csv C:\temp\sourcedomain\batch1-detail.csv

foreach ( $line in $csv ) {

$originalalias = $line.OriginalAlias
$newalias = $line.NewAlias
$newupn = $line.NewUPN
$userdisplayname = $line.DisplayName
$exchguid = $line.ExchangeGuid
$archguid = $line.ArchiveGuid
$x500address = "X500:" + $line.LegacyExchangeDN
$usersourceaddress = $originalalias + "@sourcedomain.onmicrosoft.com"
$usertargetaddress = $newupn
$usertargettenantaddress = $newalias + "@targetdomain.onmicrosoft.com"

Write-Host
Write-Host -ForegroundColor Green "Processing $usertargetaddress..."
Write-Host

New-MailUser -Name $newalias -DisplayName $userdisplayname -PrimarySmtpAddress $usertargetaddress -ExternalEmailAddress $usersourceaddress -MicrosoftOnlineServicesID $usertargetaddress -Password (ConvertTo-SecureString -String 'Hellosourcedomain2099' -AsPlainText -Force)
Set-MailUser $usertargetaddress -ExchangeGuid $exchguid -ArchiveGuid $archguid -PrimarySmtpAddress $usertargetaddress
Set-MailUser $usertargetaddress -EmailAddresses @{Add="$x500address","$usertargettenantaddress"}
Set-MailUser $usertargetaddress -EmailAddresses @{Remove="smtp:$usersourceaddress"}
Get-MailUser $usertargetaddress | fl DisplayName,PrimarySMTPAddress,ExchangeGuid,ArchiveGuid
}

Now we’ve created the MailUser objects for our batch, we can migrate them using the second file:

# create migrationbatch using csv
$batchfile = 'C:\temp\sourcedomain\batch1-users.csv'

New-MigrationBatch -Name $batchname -SourceEndpoint 'sourcedomainMigEndpoint' -CSVData ([System.IO.File]::ReadAllBytes("$batchfile")) -Autostart -TargetDeliveryDomain 'targetdomain.onmicrosoft.com'

NOTE: At this point you are migrating the mailboxes because of the “-Autostart” switch. Users in the batch will have ‘Syncing’ status. When ready to complete they will be ‘Synced’.

You can run this command to check the status of a batch:

# Get status of a migration batch:
Get-MigrationBatch $batchname | fl

Or this to check status of a single user:

# Get details for a single mailbox
Get-MigrationUser 'insertupnhere' | Get-MigrationUserStatistics | fl

Or this to get the status of all migration users, updated every 5 minutes:

# output progress of all users every 5 mins
$x = 0
Do{
$x = $x + 1
Get-MigrationUser | Get-MigrationUserStatistics

Start-Sleep -Seconds 300
}
Until ( $x -eq 480 )

When the mailboxes in a batch have ‘Synced’ status, go ahead and complete the batch using this command:

# Complete a Synced batch
Complete-MigrationBatch $batchname -Confirm:$false

This can take a bit of time so have a beer or cup of tea then come back – done! Once completed the mailuser you created in the target tenant will become a mailbox, and the mailbox in the source tenant will become a mailuser with a forwarding address set to the users target onmicrosoft.com address 👍

Some other commands I used are:

# Restart a failed batch
## Start-MigrationBatch $batchname

# remove a completed batch
## Remove-MigrationBatch $batchname -Confirm:$false

# Remove failed users from batches due to failure
## Get-MigrationUser | ? { $_.status -eq 'failed' } | Remove-MigrationUser -Confirm:$false

This migration went really smoothly… once the user was migrated, the forwarding was set correctly in the source tenant, and permissions were intact for access to Shared Mailboxes etc. Now we are planning to tidy up and move additional email aliases, the accepted domains and MX records across to the new tenant… I’m impressed with how relatively easy this was!

Until next time… ‘Hei kone ra’ from New Zealand Aotearoa! 🍻😜

Loading

Active Directory – Export Groups and Members to a CSV file (with email addresses)

Greetings! 👀 After a comment on my initial post asking for user email addresses in the output, I ended up getting a bit confused for 4 hours while trying to achieve the goal (it was a Friday night so several beers were involved) 🍻 !!

When I started seeing the dreaded pages of red errors in my results I soon realised I was not thinking that objects other than users can be members of a group. Of course! So I need to cater for computers, nested groups and users with no email address.

The result is below and from initial testing it seems to work well. Key points:

  1. As with the original script, the CSV will output AD groups and members.
  2. Where a group has no members, the group name is output with ‘No Members’ in the members column (and also now in the EmailAddress column).
  3. The CSV has an ‘EmailAddress’ column added:
    • Where the member is a user and has an email address, the address is displayed.
    • Where the member is a user and does not have an address, ‘No Email Address’ is displayed.
    • Where the member is a computer, ‘Computer Object’ is displayed.
    • Where the member is a group, ‘Nested Group’ is displayed.

Voilà mes amis ! Code is below – as usual please comment if it helped or you made it better or it didn’t work for you ✌😃🤞. Thanks for coming, until nek tiya !

Also check out the Azure AD script: export-azure-ad-groups-and-members-to-csv

# export active directory groups and members to csv (also output empty groups with 'No Members' value)
# assumes run on 2012 R2 or newer domain controller or import of ActiveDirectory module
# 2022-04-02 - added logic to output email address column, catering for other object types that do not have addresses.

$allgroups = Get-ADGroup -Filter *

$result = foreach ( $group in $allgroups ) {

    $hash = @{GroupName=$group.SamAccountName;Member='';EmailAddress=''}
    $groupid = $group.DistinguishedName
    
    if ( $members = Get-ADGroupMember $groupid ) {
            
         foreach ( $member in $members ) {
            
                if ( $member.objectClass -eq 'user' ) {
                    $memberemail = (Get-ADUser -Properties mail $member.distinguishedName).mail
                        if ( $memberemail -ne $null ) {
                            $hash.Member = $member.Name
                            $hash.EmailAddress = $memberemail
                            New-Object psObject -Property $hash
                        }
                        else {
                            $memberemail = "No Email Address"
                            $hash.Member = $member.Name
                            $hash.EmailAddress = $memberemail
                            New-Object psObject -Property $hash
                        }       
                }       
                        else {                
                            if ( $member.objectClass -eq 'group' ) {
                                $memberemail = "Nested Group"
                                $hash.Member = $member.Name
                                $hash.EmailAddress = $memberemail
                                New-Object psObject -Property $hash
                            }
                            if ( $member.objectClass -eq 'computer' ) {
                                $memberemail = "Computer Object"
                                $hash.Member = $member.Name
                                $hash.EmailAddress = $memberemail
                                New-Object psObject -Property $hash
                            }
                        }
            }
    }
        else {
        $emailaddress = "No Members"
        $displayname = "No Members"
        $hash.Member = $displayname
        $hash.EmailAddress = $emailaddress
        New-Object psObject -Property $hash
    }
}

$result | Export-Csv -Path C:\temp\ActiveDirectoryGroupsAndMembers.csv -NoTypeInformation

# End

Loading

Have you secured your IIS Web Server?

Cloud services have improved our lives and made our jobs easier – BUT they have also given hackers a worldwide platform of unlimited power with which to attack us… very sad but very true!

😲 😲 😲 😲 😲 😲 😲 😲

This makes it even more critical to secure our external-facing services as much as we can.

Hopefully you have a WAF in front of your web server, but if you are like me and have a small site that does not justify the associated costs of advanced protection, here are some basic steps to take on your Windows Server. Note that ‘Strict High Transport Security’ (step 4) is available from IIS 10 in 2019 Server.

If you’re an IT nerd like me, you just gotta be happy with a result like this from https://www.ssllabs.com/ssltest

SSL Labs test site

NOTE: When you run the test, remember to check the box if you do not want the result to be displayed on the page…

👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍

OK – to business! There are four initial steps we can perform on a personal or small business web server that is exposed to the interwebs… resulting in an A+ score from an SSL test. Note that this is ideally run from the web server with traffic allowed inbound on port 80 and 443 (you can redirect 80 to 443, but 80 does need to be open for LetsEncrypt to work without manual intervention – AFAIK 😃).

  1. Apply a Lets Encrypt certificate.

a) they are free!

b) they have a great reputation.

c) they are so easy to install it is not even funny!

Go to win-acme.com, click downloads and grab the latest version. Extract to c:\program files\win-acme. Run wacs.exe and follow the prompts… you can manually specify hostname, additional SAN names if required, or generate a wildcard. Use the default in memory validation; this creates a virtual directory (which is in memory and removed afterwards) in IIS for LetsEncrypt to connect to to verify the request. The app then automatically creates and applies a certificate that is valid for 3 months, then sets up a scheduled task to automatically renew the cert before expiry. Boom! 😁😁

Refer to https://www.win-acme.com/manual/getting-started.

2. Secure the protocols…

Open PowerShell ISE (run as admin), paste and run the code below to confirm TLS 1.0 and 1.1 are disabled and TLS 1.2 is enabled for the system and .NET:

# disable TLS 1.0 and 1.1
New-Item 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0\Server' -Force | Out-Null
New-ItemProperty -path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0\Server' -name 'Enabled' -value '0' -PropertyType 'DWord' -Force | Out-Null
New-ItemProperty -path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0\Server' -name 'DisabledByDefault' -value 1 -PropertyType 'DWord' -Force | Out-Null
New-Item 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0\Client' -Force | Out-Null
New-ItemProperty -path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0\Client' -name 'Enabled' -value '0' -PropertyType 'DWord' -Force | Out-Null
New-ItemProperty -path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0\Client' -name 'DisabledByDefault' -value 1 -PropertyType 'DWord' -Force | Out-Null
New-Item 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.1\Server' -Force | Out-Null
New-ItemProperty -path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.1\Server' -name 'Enabled' -value '0' -PropertyType 'DWord' -Force | Out-Null
New-ItemProperty -path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.1\Server' -name 'DisabledByDefault' -value 1 -PropertyType 'DWord' -Force | Out-Null
New-Item 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.1\Client' -Force | Out-Null
New-ItemProperty -path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.1\Client' -name 'Enabled' -value '0' -PropertyType 'DWord' -Force | Out-Null
New-ItemProperty -path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.1\Client' -name 'DisabledByDefault' -value 1 -PropertyType 'DWord' -Force | Out-Null

# enable TLS 1.2 for .NET
New-Item 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\.NETFramework\v4.0.30319' -Force | Out-Null
New-ItemProperty -path 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\.NETFramework\v4.0.30319' -name 'SystemDefaultTlsVersions' -value '1' -PropertyType 'DWord' -Force | Out-Null
New-ItemProperty -path 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\.NETFramework\v4.0.30319' -name 'SchUseStrongCrypto' -value '1' -PropertyType 'DWord' -Force | Out-Null
New-Item 'HKLM:\SOFTWARE\Microsoft\.NETFramework\v4.0.30319' -Force | Out-Null
New-ItemProperty -path 'HKLM:\SOFTWARE\Microsoft\.NETFramework\v4.0.30319' -name 'SystemDefaultTlsVersions' -value '1' -PropertyType 'DWord' -Force | Out-Null
New-ItemProperty -path 'HKLM:\SOFTWARE\Microsoft\.NETFramework\v4.0.30319' -name 'SchUseStrongCrypto' -value '1' -PropertyType 'DWord' -Force | Out-Null

# enable TLS 1.2 for system
New-Item 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Server' -Force | Out-Null
New-ItemProperty -path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Server' -name 'Enabled' -value '1' -PropertyType 'DWord' -Force | Out-Null
New-ItemProperty -path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Server' -name 'DisabledByDefault' -value 0 -PropertyType 'DWord' -Force | Out-Null
New-Item 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Client' -Force | Out-Null
New-ItemProperty -path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Client' -name 'Enabled' -value '1' -PropertyType 'DWord' -Force | Out-Null
New-ItemProperty -path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Client' -name 'DisabledByDefault' -value 0 -PropertyType 'DWord' -Force | Out-Null

Write-Host -ForegroundColor Green 'TLS 1.0 and 1.1 disabled. TLS 1.2 enabled.'

3. Disable insecure ciphers…

In a new ISE tab, paste the following code to disable weak ciphers (some commands may fail but that’s okay):

Disable-TlsCipherSuite -Name "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384" | Out-Null
Disable-TlsCipherSuite -Name "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256" | Out-Null
Disable-TlsCipherSuite -Name "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA" | Out-Null
Disable-TlsCipherSuite -Name "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA" | Out-Null
Disable-TlsCipherSuite -Name "TLS_DHE_RSA_WITH_AES_256_CBC_SHA" | Out-Null
Disable-TlsCipherSuite -Name "TLS_DHE_RSA_WITH_AES_128_CBC_SHA" | Out-Null
Disable-TlsCipherSuite -Name "TLS_RSA_WITH_AES_256_GCM_SHA384" | Out-Null
Disable-TlsCipherSuite -Name "TLS_RSA_WITH_AES_128_GCM_SHA256" | Out-Null
Disable-TlsCipherSuite -Name "TLS_RSA_WITH_AES_256_CBC_SHA256" | Out-Null
Disable-TlsCipherSuite -Name "TLS_RSA_WITH_AES_128_CBC_SHA256" | Out-Null
Disable-TlsCipherSuite -Name "TLS_RSA_WITH_AES_256_CBC_SHA" | Out-Null
Disable-TlsCipherSuite -Name "TLS_RSA_WITH_AES_128_CBC_SHA" | Out-Null
Disable-TlsCipherSuite -Name "TLS_RSA_WITH_3DES_EDE_CBC_SHA" | Out-Null
Disable-TlsCipherSuite -Name "TLS_DHE_DSS_WITH_AES_256_CBC_SHA256" | Out-Null
Disable-TlsCipherSuite -Name "TLS_DHE_DSS_WITH_AES_128_CBC_SHA256" | Out-Null
Disable-TlsCipherSuite -Name "TLS_DHE_DSS_WITH_AES_256_CBC_SHA" | Out-Null
Disable-TlsCipherSuite -Name "TLS_DHE_DSS_WITH_AES_128_CBC_SHA" | Out-Null
Disable-TlsCipherSuite -Name "TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA" | Out-Null
Disable-TlsCipherSuite -Name "TLS_RSA_WITH_RC4_128_SHA" | Out-Null
Disable-TlsCipherSuite -Name "TLS_RSA_WITH_RC4_128_MD5" | Out-Null
Disable-TlsCipherSuite -Name "TLS_RSA_WITH_NULL_SHA256" | Out-Null
Disable-TlsCipherSuite -Name "TLS_RSA_WITH_NULL_SHA" | Out-Null
Disable-TlsCipherSuite -Name "TLS_PSK_WITH_AES_256_GCM_SHA384" | Out-Null
Disable-TlsCipherSuite -Name "TLS_PSK_WITH_AES_128_GCM_SHA256" | Out-Null
Disable-TlsCipherSuite -Name "TLS_PSK_WITH_AES_256_CBC_SHA384" | Out-Null
Disable-TlsCipherSuite -Name "TLS_PSK_WITH_AES_128_CBC_SHA256" | Out-Null
Disable-TlsCipherSuite -Name "TLS_PSK_WITH_NULL_SHA384" | Out-Null
Disable-TlsCipherSuite -Name "TLS_PSK_WITH_NULL_SHA256" | Out-Null

Write-Host -ForegroundColor Green "Weak ciphers disabled."

4. Enable HTTP Strict Transport Security… (Windows 2019+)

a) In IIS Manager, open the HTTP Response Headers section.

b) Click Add.

c) In the Name field, add “Strict-Transport-Security“.

d) In the Value field, add “max-age=31536000” (this corresponds to a one year period validity).

d) Click OK.

Oh and don’t forget to redirect port 80 to 443… even though the above step effectively forces the browser to use HTTPS, there is no harm doing it with the URL Rewrite feature (I use this so that I can also block connections to my WordPress admin page).

Here is the rule I use at the web site level to redirect any HTTP request to HTTPS:

EDIT 02/07/22: totes forgot about step 5 – CAA (Certificate Authority Authorization) records which I enabled on this site. I use namecheap.com who offer these records free with domain name registration. Most DNS providers should support CAA records, if not, well… switch providers I say! 😎 Here is a screenshot of my records:

Superbulous! Now run the test, grab a cup of tea and a biscuit and pat yourself on the back for being so awesome!! 😎 😎 😎

Over and out until next time! Cheers 🍻

Loading

Runbook: Sync Shared Mailbox accounts with an Azure AD Group

Hey! I hope you are well.. 🤘 🙂 🤘. This script was a result of the following ponderings:

  • How to monitor and manage the deletion of Blocked (Disabled) and Guest accounts in Azure AD.
  • I have a Dynamic group for ‘Blocked (Disabled) users’, but members include valid Shared Mailbox accounts.
  • What about Guest users… should I just leave them? 🤣

Noooo, I shouldn’t… paying monthly subscriptions it’s important to stay on top of user account maintenance. There are some reports and sorting you can do, and Power BI, Graph etc, but I wanted to script something!! In my usual non-perfect PowerShell way of course, but hey it gets the job done.

The Guest users are easy to group using a Azure AD Dynamic Security group with this Rule Syntax:

(user.userType -eq "Guest") and (user.accountEnabled -eq true)

Sweet! My Blocked (or Disabled) users group is Dynamic as well, using this syntax:

(user.accountEnabled -ne true) and (user.surname -ne "Shared_Mailbox")

I’ve only just added (user.surname -ne “Shared_Mailbox”) – the script sets that attribute when it adds an account to the Shared Mailbox group, so that the accounts are excluded from the Dynamic Bocked Users group. Cool now I can actually review the blocked users knowing my Shared Mailbox accounts are safe!

I could also use Conditional Access policies to increase the security of those accounts!

Here is the script… you need to have an Automation account with credential set up (this can be a Synced AD or cloud account that has ‘Exchange Recipient’ and ‘Group Administrator’ roles assigned. Make sure you have imported the AzureAD and ExchangeOnlineManagement modules into the Automation account, and have created the Azure AD Group (set the group to ‘Assigned’ membership rather than ‘Dynamic’). From Azure AD navigate to Groups, search for your group and click on it. You will be able to copy the Object ID from here:

Enter that for $sharedmailboxgroupid and the ‘Name’ of your Automation account credential as $runbookcredentialname. That’s it – give it a good testing and whack eem into production mate!

(NB – following all relevant change control precedures of course!)

See below the code for how the output looks in the Runbook logs… great for troubleshooting!

See ya! 🍺

# use TLS 1.2
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

# Sync shared mailbox accounts with an Azure AD group - Simon Burbery - November 2021
# Update variables with the name of your runbook credential and the Azure AD Object ID displayed on the groups overview page in the AAD portal.

# set variables
$runbookcredentialname = 'svc_runbookcredential'
$sharedmailboxgroupid = '12345678-abcd-4321-0987-665544332211'

# get credential for connections
Try { 
    $CredAzure = Get-AutomationPSCredential -Name $runbookcredentialname
}
        Catch {
            Write-Error "Failed to get credential!"
            Exit
        }   
Write-Output "Get automation credential - Success"

# connect Azure AD
Try {
    Connect-AzureAD -Credential $CredAzure | Out-Null
}
        Catch {    
            Write-Error "Failed to connect to Azure AD!"
            Exit
        }
Write-Output "Connect to Azure AD - Success"

# connect EOL
Try {
    Connect-ExchangeOnline -Credential $CredAzure
}
        Catch {    
            Write-Error "Failed to connect to EOL!"
            Exit
        }
Write-Output "Connect to EOL - Success"

# get group name
$groupname = (Get-AzureADGroup -ObjectId $sharedmailboxgroupid).DisplayName

# get all shared mailboxes and group members
Write-Output "Enumerating Shared Mailbox accounts and $groupname membership..."
Try {
    $sharedmailboxaccounts = Get-Mailbox -ResultSize Unlimited | Where-Object { $_.RecipientTypeDetails -eq 'SharedMailbox' } | select ExternalDirectoryObjectID,UserPrincipalName
    $currentgroupmembers = Get-AzureADGroupMember -All $true -ObjectId $sharedmailboxgroupid | select ObjectID,UserPrincipalName
}
        Catch {    
            Write-Error "Failed to enumerate Shared Mailbox accounts or $groupname membership!"
            Exit
        }
Write-Output "Enumerate Shared Mailbox accounts and $groupname membership - Success"

# remove any members that are no longer shared mailboxes
Write-Output "Verify $groupname membership..."
Try {
    foreach ( $groupmember in $currentgroupmembers ) {
        $groupmemberid = $groupmember.ObjectID
        $groupmemberupn = $groupmember.UserPrincipalName
        $checkmember = ( $sharedmailboxaccounts.ExternalDirectoryObjectId -contains $groupmemberid )
            If ( $checkmember -ne 'True' ) {
                Write-Output "Shared Mailbox not found - removing $groupmemberupn from $groupname..."
                Remove-AzureADGroupMember -ObjectId $sharedmailboxgroupid -MemberId $groupmemberid
                Set-AzureADUser -ObjectId $groupmemberid -Surname 'Disabled User'
            }
                Else {
                    Write-Output "Shared Mailbox found - skipping $groupmemberupn"
                }
    }
}
        Catch {    
            Write-Error "Error while removing accounts from group!"
            Exit
        }
Write-Output "Verify $groupname membership - Success"

# add new shared mailbox accounts to Azure AD group
Write-Output "Checking for new Shared Mailboxes..."
Try {
    foreach ( $sharedmailboxaccount in $sharedmailboxaccounts ) {
        $sharedmailboxaccountid = $sharedmailboxaccount.ExternalDirectoryObjectId
        $sharedmailboxaccountupn = $sharedmailboxaccount.UserPrincipalName
        $checkmembersm = ( $currentgroupmembers.ObjectID -contains $sharedmailboxaccountid )
            If ( $checkmembersm -ne 'True' ) {
                Write-Output "New Shared Mailbox - adding $sharedmailboxaccountupn to $groupname..."
                Add-AzureADGroupMember -ObjectId $sharedmailboxgroupid -RefObjectId $sharedmailboxaccountid
                Set-AzureADUser -ObjectId $sharedmailboxaccountid -Surname 'Shared_Mailbox'
            }
                else {
                    Write-Output "Skipping Shared Mailbox $sharedmailboxaccountupn"
                }
    }
}
        Catch {    
            Write-Error "Error while adding accounts to group!"
            Exit
        }
Write-Output "Check for new Shared Mailboxes - Success"

# clean up
Disconnect-ExchangeOnline -Confirm:$false
Disconnect-AzureAD -Confirm:$false

# end

And here is what I really like about using Runbooks – the output from the script is available to go back and look at when failures occur etc. Nice!

Loading

Remotely trigger delta AD Connect sync!

How often do you RDP to the AD Connect server to run a Delta Sync?

Yes I know, quite often right? And that is only once you find out which server it is running on. Especially If you are in new environments a lot or someone moved it since last time… sheesh thanks for telling us Dave!! 😭🤣

This script can be run from any Windows 10, 2016 or later endpoint… it will attempt to get the servername from AD then connect remotely and run a delta sync (we do some checks and have some messaging if things fail).

🙂👍 🙂

NOTE: Update 13/12/21 – when finding the AD Connect server, if you’ve already had more than one and someone hasn’t deleted the old computer account, both names will be returned causing the script to fail. Just run that bit of code first and delete any old accounts from the domain (or just replace the code with the server name)

First thing we do is run the following commands in an elevated PowerShell prompt to add the AD PowerShell module:

Install-PackageProvider Nuget -Force #justbecause

For Windows 10/11:
Add-WindowsCapability –online –Name Rsat.ActiveDirectory.DS-LDS.Tools~~~~0.0.1.0

For Windows 2016/2019:
Install-WindowsFeature RSAT-AD-PowerShell -Confirm:$false

Next, let’s make this easy to run with elevated rights by copying the script text into notepad and saving it into the c:\_scripts folder as “Force AD Connect Sync.ps1”

Then create a “Force AD Connect Sync.cmd” on your desktop with the following in it:

start powershell.exe -ExecutionPolicy Bypass -File "c:\_scripts\Force AD Connect Sync.ps1"

Now we can right-click on the cmd file and click ‘Run as Administrator”. Does the trick and time is life!

To find the server we use a method from easy365manager, and the link for enabling remoting is from faqforge. Thanks peoples!

Easy365manager:
https://www.easy365manager.com/how-to-identify-your-azure-ad-connect-server/

Faqforge:
https://www.faqforge.com/windows/create-powershell-session-remote-computer/

Here is the script – let me know if it worked or if it sucked and how you made it better! Until next time! Cheers, Simon 🍺 …oh and PS – if you want a great rundown on AD Connect, check out Adam’s post:

https://adamtheautomator.com/azure-ad-connect/#Install_Azure_AD_Connect

# force a delta sync to Azure AD

# load AD module
Try {
    Import-Module ActiveDirectory
}
    Catch {
        Write-Warning "Encountered a problem importing AD module."
        Write-Host
        Read-Host "Press Enter to exit..."
        Exit
    }
Write-Host -ForegroundColor Green "AD module loaded successfully."
Write-Host

Try {
    $ADConnectServer = Get-ADUser -LDAPFilter "(description=*configured to synchronize to tenant*)" -Properties description | % { $_.description.SubString(142, $_.description.IndexOf(" ", 142) - 142)}
}
    Catch {
        Write-Warning "Encountered a problem obtaining name of AD Connect server."
        Write-Host
        Read-Host "Press Enter to exit..."
        Exit
    }

Write-Host -ForegroundColor Green "Found AD Connect server $ADConnectServer!  Testing connection..."
Write-Host

Try {
    $session = New-PSSession -ComputerName $ADConnectServer -Authentication Default
    Enter-PSSession $ADConnectServer
}
    Catch {
        Write-Warning "Cannot connect to $ADConnectServer, please check remote connectivity." 
        Write-Warning "ref - https://www.faqforge.com/windows/create-powershell-session-remote-computer/"
        Write-Host
        Read-Host "Press Enter to exit..."
        Exit
    }

Write-Host -ForegroundColor Green "Connected to $ADConnectServer - Forcing a delta sync... one moment!"
Write-Host

Try {
    Start-ADSyncSyncCycle -PolicyType Delta
}
    Catch {
        Write-Warning "The command failed - either a sync is already in progress," 
        Write-Warning "or you are not a member of the 'ADSyncAdmins' group on the AD Connect server."
        Write-Host
        Read-Host "Press Enter to exit..."
        Exit
    }

Write-Host -ForegroundColor Green "Sync started successfully!"
Write-Host
Read-Host "Press Enter to exit..."

# clean up
Exit-PSSession
Remove-PSSession $session

Loading

Remove proxy address for specific domain from Exchange Distribution groups

This script can be run after connecting to Exchange Online or on-premises environment. Replace “porkchops.com” with the suffix you want to remove 👍

Thanks to me mate Sailesh who loooves his porkchops!! 🤣🤣

# Remove proxy address for "porkchops.com" from Exchange Distribution groups

$domainname = "porkchops.com"

$groups = Get-DistributionGroup -Resultsize unlimited | where {$_.EmailAddresses -like "*$domainname*"} 

foreach ($group in $groups) {  

    $groupidentity = $group.identity
    $addresstoremove = $group.Alias+"@$domainname"

    Set-DistributionGroup $groupidentity -EmailAddresses @{remove=$addresstoremove}
}

# End

Loading