WSUS and PowerShell: Declining and Deleting updates based on keywords

I am back again with just another write-up on WSUS in a very short time. Last time we talked about Reporting and Cleanup, this time its more into troubleshooting, which often requires one to find particular updates and nuke them out.

Yes! We are talking about those pesky Event IDs 364, which often mention about certain cab files and we System Admin bang their heads on walls to find out that which particular updates they belong to.

Let me give you an easy permanent way out via a Custom PowerShell module named PoshWSUS.

How to use that?

Just download the module, extract the folder named PoshWSUS and copy the same to PowerShell module location for your WSUS Server (I am assuming you have Windows 2008 or Windows 2012 though it works for older ones as well).

Ok. So where is this PowerShell Module Location?

Usually it is C:\Windows\System32\WindowsPowerShell\v1.0\Modules but to know for sure, can open PowerShell prompt and Type $PSHome, which should give you C:\Windows\System32\WindowsPowerShell\v1.0

Installed the module, now what to do?

You can make use of the below code:

Import-Module PoshWSUS

Connect-PoshWSUSServer –WsusServer  -port 8530

# In case you got some Windows 2003 machine which is connect over port 80 in place of 8530 then uncomment the below line and comment the one above this comment.

# Connect-PoshWSUSServer –WsusServer  -port 80 

Get-PoshWSUSUpdate | Get-PoshWSUSUpdateFile | export-csv -notype $env:userprofile\desktop\WSUSFiles.csv

This would give you all the update names, their corresponding files, their actual disk locations and then you can easily find out, which was the particular update, which is causing you Event ID 364. Once you know that its your choice that how to deal with that update, decline, clean and approve, download again or whatever you prefer.

All well? Nope! There might be still a trouble

There is a tricky scenario as well like the one I faced once and that is Local Update Publisher. Microsoft gives the way that one can push certain non-Microsoft updates via WSUS solution after packaging it in a certain format. Looks good but may be a huge trouble when things go wrong. Updates pushed by LUP don’t show up in GUI of WSUS console, so it gets tricky to decline or clean them out. PowerShell comes handy in such scenario as by that we can find updates by keywords and then decline or delete them. Here goes the code.

[void][reflection.assembly]::LoadWithPartialName("Microsoft.UpdateServices.Administration")


$WSUS = [Microsoft.UpdateServices.Administration.AdminProxy]::getUpdateServer(,$false,8530) # Or port 80
$UpdateScope = New-Object Microsoft.UpdateServices.Administration.UpdateScope
$UpdateScope.ApprovedStates = [Microsoft.UpdateServices.Administration.ApprovedStates]::Any
$updatescope.IncludedInstallationStates = [Microsoft.UpdateServices.Administration.UpdateInstallationStates]::All
$Updates = $WSUS.GetUpdates($UpdateScope)| where-object {$_.title -like "*7-Zip*"}
# In case, the purpose is just to list patches first where title contains 7-zip

$Updates.Title.ToString()

#In case, the purpose is just to decline those patches where 7-zip comes in title, then uncomment the below line 

#$Updates.Decline()

# Uncomment the line in case you need to delete the update files and remove patch from DB as well, then uncomment the below line

#$Updates | Foreach-OBject{$WSUS.DeleteUPdate($_.Id.UpdateId.Tostring()); WRITE-host $_.Title.ToString() deleted -ForeGroundColor RED}

Hope this helps. As you know, the health of WSUS can be checked via wsusutil checkhealth and appearance of Event ID 10000 and 10030 confirm that everything is well.

WSUS and PowerShell: Audit-Compliance Report and Automatic Cleanup

Its been long since I wrote anything on WSUS. As Windows world evolved, many of the troubleshooting steps might be bit out-dated now but WSUS functioning and basics still remain more or less same.

Today I wish to take another aspect of WSUS and talk about WSUS Reporting part and regular clean-up.

WSUS Reporting:

WSUS has a comprehensive reporting system built in and if Report Viewer installed over the server, one can variety of reports, but the same remains limited to a pre-set and also one server only. Let’s think of a scenario, where one might have more than dozens of WSUS servers catering thousands of machines and rather logging over each one or attaching them into one console, we want to have a consolidated report to see if patches getting installed well and also whether team is meeting compliance by install each single patch of (M-1) month.

Now what my PowerShell code does? It bring a report like below in csv format:

Sample

This would include how many patches are needed on which machines, when those machines last contacted their respective WSUS servers, what IP and OS do they have and which particular updates are pending on them and those pending updates were released when. Also it tells count of those updates which have been downloaded or pending download or pending reboot of machines to complete the installation.

I believe this pretty much covers what a system admin might need to get an overview of issues and to address them. Apart from this, what the script does is, it creates some more CSV files, one against each WSUS server mentioning that which particular updates were released between the period of 1st day of (M-2) month (if today is March, then we talking about January 1st) and Second Monday of (M-1) month means just one day before Patch Tuesday. Based on this data, one might check if any of the last month patches are missing on any of the servers. I have written a Excel Macro code around that as well but that still needs updating and make compatible for general usage so would post that at some later time.

Also notice, in code, there are two arrays of WSUS servers. Essential difference between the two are the connection lines. One connects over 8530 port while other connects on port 80, there might be SSL and non-SSL options as well, which can get handled by the second attribute passed depending on your scenario.


# Author: Nitish Kumar
# Comprehensive report from multiple WSUS Servers
# Version 1.00

[void][reflection.assembly]::LoadWithPartialName("Microsoft.UpdateServices.Administration")
$firstdayofmonth = [datetime] ([string](get-date).AddMonths(-1).month + "/1/" + [string](get-date).year)

# Create empty arrays to contain collected data.
#$UpdateStatus = @()
$SummaryStatus = @()		

# For all WSUS servers in Environment
$WSUSNon2k3 = ("abc001","xyz001")
$WSUS2k3 = ("cba001","zyx001")

# In case need to check for some individual server or few particular servers
#$WSUSNon2k3 = ("abc001")
#$WSUS2k3 = ("")

$a0 = ($WSUSNon2k3 | measure).count + ($WSUS2k3 | measure).count
$b0 = 0

ForEach ($WS1 in $WSUSNon2k3) {
        write-host "Working on $WS1 ..."	-foregroundcolor Green
        $b0 = $b0+1

    try {
        $WSUS = [Microsoft.UpdateServices.Administration.AdminProxy]::getUpdateServer($WS1,$false,8530)
		$ComputerScope = New-Object Microsoft.UpdateServices.Administration.ComputerTargetScope
		$UpdateScope = New-Object Microsoft.UpdateServices.Administration.UpdateScope
		$UpdateScope.ApprovedStates = [Microsoft.UpdateServices.Administration.ApprovedStates]::LatestRevisionApproved
		$updatescope.IncludedInstallationStates = [Microsoft.UpdateServices.Administration.UpdateInstallationStates]::All

		$ComputerTargetGroups = $WSUS.GetComputerTargetGroups() | Where {$_.Name -eq 'All Computers'}
		$MemberOfGroup = $WSUS.getComputerTargetGroup($ComputerTargetGroups.Id).GetComputerTargets()

        write-host "Connected and Fetching the data from $WS1 for all computers connecting to it..."
        $Alldata = $WSUS.GetSummariesPerComputerTarget($updatescope, $computerscope)
        $a = ($Alldata | measure).count
        $b = 0
		write-host "Data recieved from $WS1 for all computers connecting to it..."
        Foreach ($Object in $Alldata) {
			$b = $b+1
            write-host "Getting data from number $b of all $a computers connecting to $WS1 ($b0 of $a0)..."	-foregroundcolor Yellow
            Foreach ($object1 in $MemberOfGroup) {
				If ($object.computertargetid -match $object1.id) {

					$ComputerTargetToUpdate = $WSUS.GetComputerTargetByName($object1.FullDomainName)
					$NeededUpdate = $ComputerTargetToUpdate.GetUpdateInstallationInfoPerUpdate() | where {($_.UpdateApprovalAction -eq "install") -and (($_.UpdateInstallationState -eq "Downloaded") -or ($_.UpdateInstallationState -eq "Notinstalled") -or ($_.UpdateInstallationState -eq "Failed"))	}

					$FailedUpdateReport = $null
					$NeededUpdateReport = $null
					$NeededUpdateDateReport = $null

					if ($NeededUpdate -ne $null) {
						foreach ($Update in $NeededUpdate) {

								$NeededUpdateReport += ($WSUS.GetUpdate([Guid]$update.updateid)).KnowledgebaseArticles
								$NeededUpdateDateReport += ($WSUS.GetUpdate([Guid]$update.updateid)).ArrivalDate.ToString("ddMMyyyy ")							

						}
					}

					$myObject1 = New-Object -TypeName PSObject
					$myObject1 | add-member -type Noteproperty -Name Server -Value (($object1 | select -ExpandProperty FullDomainName) -replace ".corp.sita.aero", "")
					$myObject1 | add-member -type Noteproperty -Name NotInstalledCount -Value $object.NotInstalledCount
					$myObject1 | add-member -type Noteproperty -Name NotApplicable -Value $object.NotApplicableCount
					$myObject1 | add-member -type Noteproperty -Name DownloadedCount -Value $object.DownloadedCount
					$myObject1 | add-member -type Noteproperty -Name InstalledCount -Value $object.InstalledCount
					$myObject1 | add-member -type Noteproperty -Name InstalledPendingRebootCount -Value $object.InstalledPendingRebootCount
					$myObject1 | add-member -type Noteproperty -Name FailedCount -Value $object.FailedCount
					$myObject1 | add-member -type Noteproperty -Name NeededCount -Value ($NeededUpdate | measure).count
					$myObject1 | add-member -type Noteproperty -Name Needed -Value $NeededUpdateReport
					$myObject1 | add-member -type Noteproperty -Name LastSyncTime -Value $object1.LastSyncTime
					$myObject1 | add-member -type Noteproperty -Name IPAddress -Value $object1.IPAddress
					$myObject1 | add-member -type Noteproperty -Name OS -Value $object1.OSDescription
					$myObject1 | add-member -type Noteproperty -Name NeededDate -Value $NeededUpdateDateReport
					$SummaryStatus += $myObject1
				}
			}
		}

        write-host "Connected with $WS1 and finding patches for last month schedule .."
        # Find patches from 1st day of (M-2) month to 2nd Monday of (M-1) month
		$updatescope.FromArrivalDate = [datetime](get-date).Addmonths(-2).AddDays(-((Get-date).day-1))

		$updatescope.ToArrivalDate = [datetime](0..31 | % {$firstdayofmonth.adddays($_) } | ? {$_.dayofweek -like "Mon*"})[1]

        $file1 = "$env:userprofile\desktop\Currentmonthupdates_"+$WS1+".csv"
		$WSUS.GetSummariesPerUpdate($updatescope,$computerscope) |select-object @{L='UpdateTitle';E={($WSUS.GetUpdate([guid]$_.UpdateId)).Title}},@{L='Arrival Date';E={($WSUS.GetUpdate([guid]$_.UpdateId)).ArrivalDate}},@{L='KB Article';E={($WSUS.GetUpdate([guid]$_.UpdateId)).KnowledgebaseArticles}},@{L='NeededCount';E={($_.DownloadedCount+$_.NotInstalledCount)}},DownloadedCount,NotApplicableCount,NotInstalledCount,InstalledCount,FailedCount | Export-csv -Notype $file1

        }
		catch [Exception] {
				write-host $_.Exception.GetType().FullName; -foregroundcolor Red
				write-host $_.Exception.Message; -foregroundcolor Red
                continue;
		}
		}

Foreach ($WS2 in $WSUS2k3) {
		write-host "Working on $WS2 ..."	-foregroundcolor Green
        $b0 = $b0 + 1
        try {
        $WSUS = [Microsoft.UpdateServices.Administration.AdminProxy]::getUpdateServer($WS2,$false,80)
		$ComputerScope1 = New-Object Microsoft.UpdateServices.Administration.ComputerTargetScope
		$ComputerTargetGroups1 = $WSUS.GetComputerTargetGroups() | Where {$_.Name -eq 'All Computers'}
		$MemberOfGroup = $WSUS.getComputerTargetGroup($ComputerTargetGroups1.Id).GetComputerTargets()

		$UpdateScope1 = New-Object Microsoft.UpdateServices.Administration.UpdateScope
		$UpdateScope1.ApprovedStates = [Microsoft.UpdateServices.Administration.ApprovedStates]::LatestRevisionApproved
		$updatescope1.IncludedInstallationStates = [Microsoft.UpdateServices.Administration.UpdateInstallationStates]::All

        write-host "Connected and Fetching the data from $WS2 for all computers connecting to it..."
        $Alldata = $WSUS.GetSummariesPerComputerTarget($UpdateScope1, $ComputerScope1)
        write-host "Data recieved from $WS2 for all computers connecting to it..."
        $a = ($Alldata | measure).count
        $b =0
		Foreach ($Object in $Alldata) {
        $b = $b +1
            write-host "Getting data from number $b of all $a computers connecting to $WS2 ($b0 of $a0)..."	-foregroundcolor Yellow
			Foreach ($object1 in $MemberOfGroup) {
				If ($object.computertargetid -match $object1.id) {

					$ComputerTargetToUpdate = $WSUS.GetComputerTargetByName($object1.FullDomainName)
					$NeededUpdate = $ComputerTargetToUpdate.GetUpdateInstallationInfoPerUpdate() | where {($_.UpdateApprovalAction -eq "install") -and (($_.UpdateInstallationState -eq "downloaded") -or ($_.UpdateInstallationState -eq "notinstalled"))	}

					$FailedUpdateReport = $null
					$NeededUpdateReport = $null
                    $NeededUpdateDateReport = $null

					if ($NeededUpdate -ne $null) {
						foreach ($Update in $NeededUpdate) {

								$NeededUpdateReport += ($WSUS.GetUpdate([Guid]$update.updateid)).KnowledgebaseArticles
								$NeededUpdateDateReport += ($WSUS.GetUpdate([Guid]$update.updateid)).ArrivalDate.ToString("ddMMyyyy ")
						}
					}

					$myObject1 = New-Object -TypeName PSObject
					$myObject1 | add-member -type Noteproperty -Name Server -Value (($object1 | select -ExpandProperty FullDomainName) -replace ".corp.sita.aero", "")
					$myObject1 | add-member -type Noteproperty -Name NotInstalledCount -Value $object.NotInstalledCount
					$myObject1 | add-member -type Noteproperty -Name NotApplicable -Value $object.NotApplicableCount
					$myObject1 | add-member -type Noteproperty -Name DownloadedCount -Value $object.DownloadedCount
					$myObject1 | add-member -type Noteproperty -Name InstalledCount -Value $object.InstalledCount
					$myObject1 | add-member -type Noteproperty -Name InstalledPendingRebootCount -Value $object.InstalledPendingRebootCount
					$myObject1 | add-member -type Noteproperty -Name FailedCount -Value $object.FailedCount
					$myObject1 | add-member -type Noteproperty -Name NeededCount -Value ($NeededUpdate | measure).count
					$myObject1 | add-member -type Noteproperty -Name Needed -Value $NeededUpdateReport
					$myObject1 | add-member -type Noteproperty -Name LastSyncTime -Value $object1.LastSyncTime
					$myObject1 | add-member -type Noteproperty -Name IPAddress -Value $object1.IPAddress
					$myObject1 | add-member -type Noteproperty -Name OS -Value $object1.OSDescription
					$myObject1 | add-member -type Noteproperty -Name NeededDate -Value $NeededUpdateDateReport
					$SummaryStatus += $myObject1
				}
			}
		}

		$SummaryStatus | sort server |select-object server,NeededCount,LastSyncTime,InstalledPendingRebootCount,NotInstalledCount,DownloadedCount,InstalledCount,FailedCount,@{Name="KB Numbers"; Expression = {$_.Needed}},@{Name="Arrival Date"; Expression = {$_.NeededDate}},NotApplicable,IPAddress,OS|export-csv -notype $Env:Userprofile\desktop\AllServersStatus.csv

        write-host "Connected with $WS2 and finding patches for last month schedule .."
		# Find patches from 1st day of (M-2) month to 2nd Monday of (M-1) month
		$updatescope1.FromArrivalDate = [datetime](get-date).Addmonths(-2).AddDays(-((Get-date).day-1))

		$updatescope1.ToArrivalDate = [datetime](0..31 | % {$firstdayofmonth.adddays($_) } | ? {$_.dayofweek -like "Mon*"})[1]

        $file2 = "$env:userprofile\desktop\Currentmonthupdates_"+$WS2+".csv"

		$WSUS.GetSummariesPerUpdate($updatescope1,$computerscope1) |select-object @{L='UpdateTitle';E={($WSUS.GetUpdate([guid]$_.UpdateId)).Title}},@{L='Arrival Date';E={($WSUS.GetUpdate([guid]$_.UpdateId)).ArrivalDate}},@{L='KB Article';E={($WSUS.GetUpdate([guid]$_.UpdateId)).KnowledgebaseArticles}},@{L='NeededCount';E={($_.DownloadedCount+$_.NotInstalledCount)}},DownloadedCount,NotApplicableCount,NotInstalledCount,InstalledCount,FailedCount | Export-csv -Notype $file2
        }
		catch [Exception] {
				write-host $_.Exception.GetType().FullName; -foregroundcolor Red
				write-host $_.Exception.Message; -foregroundcolor Red
                continue;
		}
}

Once the report task is done, let’s talk about one smaller one. Cleaning of WSUS not relevant files and Computers. WSUS itself provides a one click option for this but again, it doesn’t cater if you want to handle multiple servers at once nor gives you a text report of what happened in one pass. So, here goes the another script to solve that part.

Now this script considers that we are in a SCOM monitored (or may be some other tool) environment means we would get a few tickets when we run the clean-up as clean-up restarts the update service on each WSUS twice and also may trigger high CPU etc. The script stops before each WSUS server and takes a manual input from you while informing you that its going to work on which server and that server should be put into maintenance mode in SCOM before proceeding further. It logs everything into the same directory where the script is. The path can be changed in case needed.

Also if one wants to make it completely automated then read-host lines can be removed and also at the end of code, we can add some lines to ensure that the status of entire activity gets passed over email to relevant personnel.

# Author : Nitish Kumar
# Performs a cleanup of WSUS.
# Outputs the results to a text file.
# version 1.0
# 07 March 2017

[void][reflection.assembly]::LoadWithPartialName("Microsoft.UpdateServices.Administration") 

# For all WSUS servers in Environment
$WSUSNon2k3 = ("abc001","xyz001")
$WSUS2k3 = ("cba001","zyx001")

# In case need to check for some individual server or few particular servers
#$WSUSNon2k3 = ("abc001")
#$WSUS2k3 = ("")

$AllWSUSCount = ($WSUSNon2k3 | measure).count + ($WSUS2k3 | measure).count
$WorkingonWSUS = 0

$thisDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$logFile = "WSUSCleanupResults_$((Get-Date).ToString('MM-dd-yyyy_hh-mm-ss')).txt"
Start-Transcript -Path $thisDir\$logFile

$Reply = "@"

ForEach ($WS1 in $WSUSNon2k3) {
        $WorkingonWSUS = $WorkingonWSUS + 1
        $WS1

		Read-Host "Put $WS1 into maintenance mode. Once done,press Enter to continue ..." | Out-Null

        write-host "Working on $WS1 ($WorkingonWSUS of $AllWSUSCount) ..."	-foregroundcolor Green
try { 

    $WSUS = [Microsoft.UpdateServices.Administration.AdminProxy]::getUpdateServer($WS1,$false,8530)

	write-host "Connected with $WS1 ($WorkingonWSUS of $AllWSUSCount)and proceeding for cleanup ..."	-foregroundcolor Yellow
	write-host "Connected with $WS1 ($WorkingonWSUS of $AllWSUSCount)and proceeding for cleanup ..."

	$cleanupScope = new-object Microsoft.UpdateServices.Administration.CleanupScope; 

	$cleanupScope.CleanupObsoleteComputers = $true
	$cleanupScope.DeclineSupersededUpdates = $true
	$cleanupScope.DeclineExpiredUpdates         = $true
	$cleanupScope.CleanupObsoleteUpdates     = $true
	$cleanupScope.CleanupUnneededContentFiles = $true
	$cleanupScope.CompressUpdates                  = $true 

	$cleanupManager = $WSUS.GetCleanupManager();
	$cleanupManager.PerformCleanup($cleanupScope); 

	write-host "Cleaning done for $WS1 ($WorkingonWSUS of $AllWSUSCount) ..."	-foregroundcolor Yellow
}
catch [Exception] {

	write-host $_.Exception.GetType().FullName -foregroundcolor Red
	write-host $_.Exception.Message -foregroundcolor Red

	continue;
}
}

ForEach ($WS1 in $WSUS2k3) {
        $WorkingonWSUS = $WorkingonWSUS + 1
        $WS1

		Read-Host "Put $WS1 into maintenance mode. Once done,press Enter to continue ..." | Out-Null		

        write-host "Working on $WS1 ($WorkingonWSUS of $AllWSUSCount) ..."	-foregroundcolor Green

try { 

    $WSUS = [Microsoft.UpdateServices.Administration.AdminProxy]::getUpdateServer($WS1,$false,80)

	write-host "Connected with $WS1 ($WorkingonWSUS of $AllWSUSCount)and proceeding for cleanup ..."	-foregroundcolor Yellow

	$cleanupScope = new-object Microsoft.UpdateServices.Administration.CleanupScope; 

	$cleanupScope.CleanupObsoleteComputers = $true
	$cleanupScope.DeclineSupersededUpdates = $true
	$cleanupScope.DeclineExpiredUpdates         = $true
	$cleanupScope.CleanupObsoleteUpdates     = $true
	$cleanupScope.CleanupUnneededContentFiles = $true
	$cleanupScope.CompressUpdates                  = $true 

	$cleanupManager = $WSUS.GetCleanupManager();
	$cleanupManager.PerformCleanup($cleanupScope);
	write-host "Cleaning done for $WS1 ($WorkingonWSUS of $AllWSUSCount) ..."	-foregroundcolor Yellow

}
catch [Exception] {

	write-host $_.Exception.GetType().FullName -foregroundcolor Red
	write-host $_.Exception.Message -foregroundcolor Red

	continue;
}
}
Stop-Transcript
notepad $logFile

So these were my two cents. These scripts are still a work-in-progress and would improve based on feedbacks.

PowerShell Report of Office 365 Licenses Assignments

In the last post, we talked about reporting how many users you created yesterday or in last 7 days or may be in last six months. A similar requirement is to know how many Office 365 licenses you assigned in last 24 hrs or in last 7 days. Not really perfect way of reporting the same but a workaround PowerShell script for the same purpose might be as given below:

# !Author! Nitish Kumar

$file = "c:\temp\NewUsersLicense_$((Get-Date).ToString('MM-dd-yyyy_hh-mm-ss')).csv"
# If Okay with storing password in the script
$User = "DummyUserName"
$password = 'DummyPassword' | ConvertTo-SecureString -asPlainText -Force
$cred= New-Object System.Management.Automation.PSCredential ($User, $password )

# Uncomment the below line and comment three lines above it if want to take credentials as input
# $cred = Get-Credential
$s = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://ps.outlook.com/powershell -Credential $cred -Authentication Basic –AllowRedirection
Import-PSSession $s

Get-Mailbox -resultsize unlimited | where {$_.WhenMailboxCreated -gt (get-date).adddays(-1)} |Select-Object DisplayName,UserPrincipalName,WindowsEmailAddress,whenMailboxCreated,MailboxPlan | Export-Csv "$file" -noTypeInformation

$style = "
<style>BODY{font-family: Arial; font-size: 10pt;}"
$style = $style + "TABLE{border: 1px solid black; border-collapse: collapse;}"
$style = $style + "TH{border: 1px solid black; background: #dddddd; padding: 5px; }"
$style = $style + "TD{border: 1px solid black; padding: 5px; }"
$style = $style + "</style>

"

$smtpServer = "Your SMTP Server URL/ IP"
$att = new-object Net.Mail.Attachment($file)
$msg = new-object Net.Mail.MailMessage
$smtp = new-object Net.Mail.SmtpClient($smtpServer)
$msg.From = "Your_From_Email_ID here"
$msg.To.Add("Your_To_Email_ID here")
$msg.Subject = "Report of Licenses assigned in last 24 hrs"
$body = "<b>Dear Team</b>"
$body += "Attached are the Report of Licenses assigned in last 24 hrs"
$body += Import-Csv -Path $file | ConvertTo-Html -Head $style
$body += ""
$body += "<b>Regards</b>"
$msg.Body = $body
$msg.IsBodyHTML = $true
$msg.Attachments.Add($att)
$smtp.Send($msg)
$att.Dispose()

#End of script

The issue with the above script is only one, it needs the password in Plain Text to be stored in the script to be automated and in case one doesn’t want it to be automated then can work with the alternate way mentioned in the script (Commented right now).

Finally a group trip to Agra blessed with good weather luckily

What kind of year puts 2nd October, Christmas, Diwali all on Sundays? Yeah.. its current year 2016 only, which left the cult status holding August mid as almost the last long holiday to roam places for professionals across India. Result??? Almost each popular destination across Metro was supposed to be flooded with desperate crowd looking to have a good time.

This year cult August Mid had kind of double treat considering not only Monday was off but a Thursday too, means those who missed the first long weekend Sat-Sun-Mon, were allowed to make it into the second weekend after taking just one day Friday off ending up having Thu-Fri-Sat-Sun. The same scenario enabled those offices with multiple shifts overcoming the issue of only one of them getting the chance and others slogging through weekends. But for me, it was still tough considering the question that where to go?

Been to Lansdowne few weeks back, been to Nainital in April end, been to Dharamshala in mid of March, been to Manali in last December, been to Rishikesh before that, Mussoorie trip would have been of no point, then where to go? Sitting back home was kind of becoming only option considering unavailability of hotels in all major hills around Delhi, but then knowing that there is no other long weekend left this year, we had to go for some trip.

DSCN8088

We explored the idea of a Chail monsoon trip as never been to Shimla side but then colleague from Mohali and others who been to that area warned about the kind of roads in last leg of journey during unpredictable Monsoon. Same time hotel rates and unavailability in general became last nail to coffin of another Shimla side plans. (That side trip always getting dropped in planning stage since a while, let’s see when I manage to change that.)

Agra was selected based on shortest possible distance though I been to Agra once (before marriage), wife been to Agra once (before marriage), brother been to Agra once last year itself and the only partners who haven’t been to Agra were brother’s wife and my daughter. That was enough excuse for us to go for another trip.

Booking hotel through GOIbibo was another ordeal. Brother booked the same and at first Hotel Karan Vilas was confirmed by GOIbibo but brother got call from hotel that they don’t have any rooms available. On reaching out to GoIbibo support, he was ridiculously asked to cancel the booking and bear the charges of 500 Rs on him, which sure was enough to make us furious as what fault was on our part?

Later after discussing with seniors in GoIbibo support, we been offered Hotel Orchid. Hotel Karan Vilas which we had selected was on Main road, had swimming pools etc while this one was little off-road and without swimming pool etc so this was sure a rip-off on us but we okayed it at the end. Though later on during the trip, we got call from same Hotel Karan Vilas that they are holding two rooms for us. Don’t know where the miscommunication was but then we ended up in Hotel Orchid only.

Continue reading “Finally a group trip to Agra blessed with good weather luckily”

Single day family trip to Lansdowne

Don’t know if the trip to Dharamshala side has raised the bars or I am out of luck about hill trips in recent that not getting enough satisfaction from the hill drives. After last hill trip in April, last week of June 2016 seen another quick hill trip and this time destination was Lansdowne.

The plan for the trip came in mind as brother’s wife was dropping by for two days and we planned to take around hills. Later on, it was realized that it would be too hectic for her and the plan was dropped. But too late to save me from the poison of travel bug…..

We decided to take on the hill trip one week before it was planned and even though wanted to accommodate brother as well, it was again a family trip only.

DSCN7949


As been a norm for our hill trips, we started early from our apartment at Faridabad and first stop was taken at around 0530 hrs near Modinagar.

IMG_1403

The route may be of less than 270kms, don’t expect to cover the same in less than 6 hours, thanks to road and traffic conditions. Not some kind of worse but not super highways either. We got stuck for few mins at Najibabad area, where Bus station/ Railways station by the road were enough to create jam.

IMG_1411

Before 0900 hrs, we had crossed Kotdwara which is the point from where hills start. The above pictures were taken around Mankot, which was few kms before.

DSCN7950

And almost after 10kms another stop. Roads were ok in condition though on narrower side. Would have been trouble if both side traffic though that day being kind of non-season may be and early morning, it was manageable though bottleneck to speed. After taking many a stops, we might have reached there by some 1100 hrs.

Parking wasn’t tough though for my surprise, it was sufficiently crowded. Bigger surprise was the hotel room tariffs as none of them was either available or less than even 2000 Rs a night. This was sure high for such a small hill station as I expected such rates in Shimla etc but not in Lansdowne at least. Before even starting to explore the area, the decision was made that it would be single day trip than two days as it was earlier planned.

We stopped at good restaurant for the Aaloo-Paneer Paranthas and tea and then moved towards the lake.

DSCN7957DSCN7958

The lake, which even though might be looking ok in picture, wasn’t in good conditions in this season at least. Dirty and stopped water, not sure how it remains in other seasons or post rains. Good thing about it was proper parking place, nearby lawn etc.

DSCN7955DSCN7968DSCN7962

Don’t remember exactly but Entry ticket prices were nominal and even boating prices were ok though we chosen not to do boating (time constraint and disliking for the water here).

DSCN7986

There was a rabbit house in the lawn side which was the main attraction for kiddo.

DSCN7956DSCN7988

After spending almost an hour there, we moved towards tiffin top. After driving a little, there comes a place, where the road to tiffin top was mentioned but that seems to be for peddlers. Though some of the vehicles been managing to drive through there, I wasn’t too sure about it. Some other families like us been trying to enquire about it and soon a local guy passing by informed us about the main route for vehicles.

DSCN7992

DSCN7990

Views weren’t that clear but honestly speaking, the place looked good about sunrise and sunset only and unfortunately we didn’t had time for both. Parking options were bad at the place though we managed it. Must have left the spot within 30 mins and then decided to head back to home entirely.

DSCN8005

The above was at around 1430 hrs and it was still cloudy day all around.

DSCN8021

We took a break at some under construction hotel in midway. Many laborers been cleaning the river and I was thinking what if State Govt itself does the widening/ cleaning works on rivers under MNREGA etc. Important task considering the tourism etc, but then who know NGT ego may be hurt again to pass another random Tuglaq style order.

DSCN8022

The place would be good photo stop in coming days.

The good times were to end once we were in plains. Area around Modinagar was hell for driving due to long traffic queues and despite of our tries, we reached home by 1000 hrs around only. It would have been 8hrs journey back to home but return journeys always feel longer. The good part was, I had a day off on next day.

Another hill trip was over and I was still unsatisfied from hills while winter seasons and snow still far away. Manali side anyway has been destroyed by NGT tantrums.

Active Directory Reporting by Emails via Powershell

Monitoring a large Active Directory environment always comes with so many requirements at times. Some of these might be based on various business requirements while some of them are meant for being proactive over the changes in the environment based on daily activities. All the codes applicable to Windows 2003 environment but should work for higher than that as well.

Let’s visit some of the Powershell scripts which might come useful at times:

Dump of all AD User’s info + group Membership of each user along with all relevant attributes

The below code isn’t sending HTML email, but plain text with two attachments only. We can schedule the same weekly basis or fortnight basis to keep snapshots of AD on regular basis to refer at any point of time in cases of requirement.

  
# !Author! Nitish Kumar

#Would talk about pre-requisites for importing ActiveDirectory Module at end of post 
import-module ActiveDirectory  

#Preparing files to write data and attach to email
$file1 = "c:\temp\AllADusers_$((Get-Date).ToString('MM-dd-yyyy_hh-mm-ss')).csv"  
$file2 = "c:\temp\GroupMembershipDetails_$((Get-Date).ToString('MM-dd-yyyy_hh-mm-ss')).csv"  

#Powershell command to fetch all AD Users data, Columns can be added/ removed
Get-ADUser -Properties * -filter *| select SamAccountName,CN,DisplayName,EmailAddress,MobilePhone,Department,City,Company,Enabled,ObjectClass,Created,msExchWhenMailboxCreated,Modified,LastLogonDate,LastBadPasswordAttempt,PasswordLastSet,PasswordNeverExpires,AccountExpirationDate,BadLogonCount,Manager,DistinguishedName,whenCreated | export-csv "$file1" -noTypeInformation   

#Powershell command to fetch group memberships of each single user in AD
Get-ADUser -Filter * -Properties SamAccountName,DisplayName,memberof | % { New-Object PSObject -Property @{ DomainID = $_.SamAccountName UserName = $_.DisplayName Groups = ($_.memberof | Get-ADGroup | Select -ExpandProperty Name) -join "," } } | Select DomainID,UserName,Groups | Export-Csv "$file2" -noTypeInformation   

#Email related settings
$smtpServer = "mail.xyz.com"  
$att1 = new-object Net.Mail.Attachment($file1)  
$att2 = new-object Net.Mail.Attachment($file2)  
$msg = new-object Net.Mail.MailMessage  
$smtp = new-object Net.Mail.SmtpClient($smtpServer)  
$msg.From = "email_1@xyz.com"  
$msg.To.Add("email_2@xyz.com")  
$msg.To.Add("email_3@xyz.com")  
$msg.Subject = "All AD Users Info"  
$msg.Body = "Attached is the All AD Users Report and Group memberships."  
$msg.Attachments.Add($att1)  
$msg.Attachments.Add($att2)  
$smtp.Send($msg)  
$att1.Dispose()  
$att2.Dispose()  

#Script ends here

Little modification in the script and one can get any number of columns or related info. The generated files can even be used for restoring things in cases of emergency, but we wouldn’t be discussing that part as of now.

HTML Report of All Users created in last one day, last week, last month

Getting updates on daily, weekly, monthly basis that how many news users have been added into your infra can be quite useful at times. Below code serves for the purpose of getting report of all users created in last 24 hrs.

  
# !Author! Nitish Kumar

#Would talk about pre-requisites for importing ActiveDirectory Module at end of post 
import-module ActiveDirectory

$file = "c:\temp\NewUsers_$((Get-Date).ToString('MM-dd-yyyy_hh-mm-ss')).csv" 

$When = ((Get-Date).AddDays(-1)).Date
Get-ADUser -Filter {whenCreated -ge $When} -Properties *| select SamAccountName,EmailAddress,whenCreated |Export-Csv "$file" -noTypeInformation

$style = "<style>BODY{font-family: Arial; font-size: 10pt;}"
$style = $style + "TABLE{border: 1px solid black; border-collapse: collapse;}"
$style = $style + "TH{border: 1px solid black; background: #dddddd; padding: 5px; }"
$style = $style + "TD{border: 1px solid black; padding: 5px; }"
$style = $style + "</style>"

$smtpServer = "mail.xyz.com"
$att = new-object Net.Mail.Attachment($file)
$msg = new-object Net.Mail.MailMessage
$smtp = new-object Net.Mail.SmtpClient($smtpServer)
$msg.From = "email_1@xyz.com"
$msg.To.Add("email_2@xyz.com")
$msg.Subject = "Report of users created in last 24 hrs"
$body = "<b>Dear Team</b><br>"
$body += "Attached is the Report of users and their emails created today <br><br>"
$body += Get-ADUser -Filter {whenCreated -ge $When} -Properties *| Select-Object @{Label=”Domain ID”;Expression={($_.SamAccountName)}},@{Label=”User Name”;Expression={($_.Name)}},@{Label=”Email Address”;Expression={($_.EmailAddress)}}, @{Label=”Created on”;Expression={($_.whenCreated)}} | ConvertTo-Html -Head $style
$body += "<br><b>Regards</b><br>"
$body += "IT Team <br>"
$msg.Body = $body
$msg.IsBodyHTML = $true
$msg.Attachments.Add($att)
$smtp.Send($msg)
$att.Dispose()

#End of script

By changing $When variable to 1, 7, 30, you can get the desired results (would require to modify the email message accordingly).

HTML Report of All Users deleted in last one day, last week, last month, last six months

When we talking about notifications on users created in last 24 hrs, last 7 days, last month, then we would sure like to have similar reports about the users who got deleted. Here we go

  
# !Author! Nitish Kumar

#Would talk about pre-requisites for importing ActiveDirectory Module at end of post 
import-module ActiveDirectory

$file = "c:\temp\Last1DaysDeletedUsers_$((Get-Date).ToString('MM-dd-yyyy_hh-mm-ss')).csv" 

$When = ((Get-Date).AddDays(-1)).Date

Get-ADObject -Filter {(isdeleted -eq $true) -and (name -ne "Deleted Objects") -and (objectclass -eq "user")} -includeDeletedObjects -property * | Where-Object {$_.whenChanged -gt $When} | Sort-Object whenChanged| Select-Object @{Label=”Domain ID”;Expression={($_.SamAccountName)}}, @{Label=”User Name”;Expression={($_.Name)}}, @{Label=”Deleted on”;Expression={($_.whenChanged)}} |Export-Csv "$file" -noTypeInformation

$style = "<style>BODY{font-family: Arial; font-size: 10pt;}"
$style = $style + "TABLE{border: 1px solid black; border-collapse: collapse;}"
$style = $style + "TH{border: 1px solid black; background: #dddddd; padding: 5px; }"
$style = $style + "TD{border: 1px solid black; padding: 5px; }"
$style = $style + "</style>"

$smtpServer = "mail.xyz.com"
$att = new-object Net.Mail.Attachment($file)
$msg = new-object Net.Mail.MailMessage
$smtp = new-object Net.Mail.SmtpClient($smtpServer)
$msg.From = "email_1@xyz.com"
$msg.To.Add("email_2@xyz.com")
$msg.Subject = "Report of Users Deleted in last 24 hours"
$body = "<b>Dear Team</b><br>"
$body += "Attached is the Report of users deleted in last 24 hours. <br><br>"
$body += Get-ADObject -Filter {(isdeleted -eq $true) -and (name -ne "Deleted Objects") -and (objectclass -eq "user")} -includeDeletedObjects -property * | Where-Object {$_.whenChanged -gt $When}  | Sort-Object whenChanged| Select-Object @{Label=”Domain ID”;Expression={($_.SamAccountName)}}, @{Label=”User Name”;Expression={($_.Name)}}, @{Label=”Deleted on”;Expression={($_.whenChanged)}} | ConvertTo-Html -Head $style
$body += "<br><b>Regards</b><br>"
$body += "IT Team<br>"
$msg.Body = $body
$msg.IsBodyHTML = $true
$msg.Attachments.Add($att)
$smtp.Send($msg)
$att.Dispose()

#End of script

Similarly by changing $When variable, we can get information for last 24 hrs, 7 days, 30 days or 180 days (max possible).

HTML Report of All Users disabled in last one day, last week, last month, last six months

Many Organizations disable the user ID first rather than deleting the same, so one might need the reports of users disabled in last 24 hours, last week or last month. Here we go …

  
# !Author! Nitish Kumar

#Would talk about pre-requisites for importing ActiveDirectory Module at end of post 
import-module ActiveDirectory

$file = "c:\temp\DisabledUsers_$((Get-Date).ToString('MM-dd-yyyy_hh-mm-ss')).csv" 

$When = ((Get-Date).AddDays(-1)).Date

Get-ADUser -LDAPFilter {(useraccountcontrol:1.2.840.113556.1.4.803:=2)} -Properties whenChanged | Where-Object {$_.whenChanged -gt $When} | Select-Object @{Label=”Domain ID”;Expression={($_.SamAccountName)}},@{Label=”User Name”;Expression={($_.Name)}}, @{Label=”Disabled on”;Expression={($_.whenChanged)}} |Export-Csv "$file" -noTypeInformation

$style = "<style>BODY{font-family: Arial; font-size: 10pt;}"
$style = $style + "TABLE{border: 1px solid black; border-collapse: collapse;}"
$style = $style + "TH{border: 1px solid black; background: #dddddd; padding: 5px; }"
$style = $style + "TD{border: 1px solid black; padding: 5px; }"
$style = $style + "</style>"

#Settings related to mail
$smtpServer = "mail.xyz.com"
$att = new-object Net.Mail.Attachment($file)
$msg = new-object Net.Mail.MailMessage
$smtp = new-object Net.Mail.SmtpClient($smtpServer)
$msg.From = "email_1@xyz.com"
$msg.To.Add("email_1@xyz.com")
$msg.Subject = "Report of Users disabled in last 24 hours."
$body = "<b>Dear Team</b><br>"
$body += "Attached is the Report of users disabled in last 24 hours. <br><br>"
$body += Get-ADUser -LDAPFilter {(useraccountcontrol:1.2.840.113556.1.4.803:=2)} -Properties whenChanged | Where-Object {$_.whenChanged -gt $When} | Select-Object @{Label=”Domain ID”;Expression={($_.SamAccountName)}},@{Label=”User Name”;Expression={($_.Name)}}, @{Label=”Disabled on”;Expression={($_.whenChanged)}} | ConvertTo-Html -Head $style
$body += "<br><b>Regards</b><br>"
$body += "IT Team <br>"
$msg.Body = $body
$msg.IsBodyHTML = $true
$msg.Attachments.Add($att)
$smtp.Send($msg)
$att.Dispose()

Its for disabled users in last 24 hours. One can get report of users disabled in last week and last month accordingly by changing $When variable…

Disabling a user and removing it from all the groups it is member of

Let’s say someone has left the organization and you want to disable the ID along with removing all the groups he/ she is member of. It takes SamAccountName of the concerned person as input …

  
# !Author! Nitish Kumar

import-module ActiveDirectory

$ErrorActionPreference = "SilentlyContinue"
$user = Read-Host -Prompt 'Input Domain ID to Delete: '
Disable-ADAccount -Identity $user
Get-ADPrincipalGroupMembership $user | select -ExpandProperty Name | foreach-object{  remove-adgroupmember -identity $_ -member $user -Confirm:$False  }

Getting emails of a number of users

Don’t know if IT guys in other companies get such kind of requests but it was a frequent case at my office that we would get a huge list of users (their SamAccountNames) and would be asked to provide email IDs against them.

Easier option to accomplish the same is to do a vlookup from the AD data (as obtained by the last script), but in case one directly wants updated data from AD then the below script can be used.

  
# !Author! Nitish Kumar

Get-Content c:\temp\user.txt | ForEach {Get-ADUser -Identity $_ -Properties DisplayName,EmailAddress |Select SamAccountName,DisplayName,EmailAddress} | Export-Csv c:\temp\EmailAddresses1.csv -NoTypeInformation

Distribution group members and their emails

Many times you might require the updated list of members along with their Email IDs for a given distribution group. The below command takes display name of Distribution Group and gives output at c:\temp

  
# !Author! Nitish Kumar

import-module ActiveDirectory

$group = Read-Host -Prompt 'Input Group Display Name'
$file = "c:\temp\"+$group+"_$((Get-Date).ToString('MM-dd-yyyy_hh-mm-ss')).csv" 

Get-ADGroupMember -Identity $group -Recursive | Get-ADUser -Properties Mail | Select-Object @{Label=”Domain ID”;Expression={($_.SamAccountName)}},@{Label=”User Name”;Expression={($_.Name)}},@{Label=”Email Address”;Expression={($_.Mail)}} |Export-Csv "$file" -noTypeInformation

Pre-requisites for import-module ActiveDirectory

While its non-issue for the environments where you have Windows 2012 based ActiveDirecory servers, in case of Windows 2003/2008 only domains, one requires to install the below package on one server in the domain at least (Personally I found that its safer to install the same on more than one servers).

https://www.microsoft.com/en-in/download/details.aspx?id=2852

In case of Windows 2003 installing the above package isn’t enough. .Net 3.5.1 should be installed along with patches. The below hotfix also needs to be installed on all servers over which above package is installed.

https://support.microsoft.com/en-us/kb/969166

If failing to find the hotfix as Windows 2003 is out of support now then you can get the same directly from here

https://1drv.ms/u/s!Ag5VPL5rMr_Nh4thXPxHby0MJ_LjXQ

If you facing issues in installation of any one of above, then may be you require to replace the .dll at location C:\WINDOWS\Assembly\GAC_MSIL\System.DirectoryServices.AccountManagement\3.5.0.0__b77a5c561934e089 with the given below one as was my case:

https://1drv.ms/u/s!Ag5VPL5rMr_Nh4tg1xMjunTGVxPYyQ

Build Your Admin Workstation

Install the appropriate version of Windows RSAT.
Add these Windows RSAT features bolded below (Control Panel, Programs, Turn Windows features on or off):
Remote Server Administration Tools
– Role Administration Tools
– – AD DS and AD LDS Tools
– – – Active Directory Module for Windows PowerShell
– – – AD DS Tools
– – – – Active Directory Administrative Center
– – – – AD DS Snap-ins and Command-line Tools

In case you still face the issue then below is the Technet Article

https://blogs.technet.microsoft.com/ashleymcglone/2011/03/17/step-by-step-how-to-use-active-directory-powershell-cmdlets-against-2003-domain-controllers/