Wednesday, November 26, 2014

Testing IP address validity in PowerShell

There may come a time when you want to test if an IP-address is valid or not. Here's a simple PowerShell function that will do just that and return true or false.

Function Test-IPAddress {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory=$True, ValueFromPipelineByPropertyName=$True, Position=0)]
        [String]$IPAddress
    )
   
    Begin {
    }

    Process {
        Return ($IPAddress -As [IPAddress]) -As [Bool]
    }

    End {
    }
}



Thursday, August 22, 2013

Install Windows Updates using a PowerShell script

or: How I stopped worrying and learned to live with Automatic Maintenance


As many of you may already know Windows Server 2012 does not play nice with WSUS/Windows Update. It will accept the download and notify or the download and schedule settings, but that's about where it ends. In other words, it will not honor the Windows Update schedule settings.

This is due to a new feature called Automatic Maintenance, which manages software updates in its own special kind of way and prevents us from forcing a reboot immediately after the updates have been installed.

If you are not yet sure how it works, this posts sums it up pretty well:
http://social.technet.microsoft.com/Forums/windowsserver/en-US/9714c384-ffed-4561-8349-32fd1dace9f9/server-2012-windows-update-group-policy


To work around this behavior and make sure that the updates get installed and the servers immediately rebooted I put together this PowerShell script. Together with the Windows Update policy to just download and notify it should at least ensure that the Windows Updates get installed during the proper server maintenance window.

I'm putting this script up here as-is and leave no guarantees that it's either perfect or will work for you. As always, watch for line breaks. If you find any bugs or have ideas on how to improve it, feel free to leave a comment.


[CmdletBinding()]
Param (
    [switch]$Reboot
)

$UpdateSession = New-Object -ComObject 'Microsoft.Update.Session'
$UpdateSearcher = $UpdateSession.CreateUpdateSearcher()
$SearchResult = $UpdateSearcher.Search("IsInstalled=0 and IsHidden=0")
$RebootRequired = $False

$Install = New-Object -ComObject 'Microsoft.Update.UpdateColl'
for($i=0;$i -lt $SearchResult.Updates.Count;$i++) {
    $Update = $SearchResult.Updates.Item($i)
    Write-Verbose $Update.Title
    if($Update.InstallationBehavior.CanRequestUserInput) {
        Write-Verbose 'CanRequestUserInput True'
        Continue
    }
    if($Update.EulaAccepted -eq $False) {
        Write-Verbose 'Accepting EULA'
        $Update.AcceptEula()
    }
    if($Update.IsDownloaded) {
        Write-Verbose 'IsDownloaded'
        $Install.Add($Update) | Out-Null
        if($Update.InstallationBehavior.RebootBehavior -gt 0) {
            $RebootRequired = $True
        }
    } else {
        Write-Verbose 'NotDownloaded'
    }
}

if($Install.Count -eq 0) {
    Write-Verbose 'No updates to install'
    break
}

$UpdateInstaller = $UpdateSession.CreateUpdateInstaller()
$UpdateInstaller.Updates = $Install
$Results = $UpdateInstaller.Install()

$Date = (Get-Date).ToUniversalTime()
$DateUTC = Get-Date($Date) -Format 'yyyy-MM-dd HH:mm:ss'

$LastSuccessTime = $DateUTC
$LastError = $Results.HResult

Write-Verbose $Results

$RegPath = 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\Results\Install'
if(Test-Path -Path $RegPath) {
    $RegKey = Get-ItemProperty -Path $RegPath
    if($RegKey -match 'LastError') {
        Set-ItemProperty -Path $RegPath -Name 'LastError' -Value $LastError
    }
    if($RegKey -match 'LastSuccessTime') {
        Set-ItemProperty -Path $RegPath -Name 'LastSuccessTime' -Value $LastSuccessTime
    }
}

if($RebootRequired -or $Reboot) {
    Write-Verbose 'Rebooting in 10 seconds'
    Start-Sleep -Seconds 10
    Restart-Computer -Force
}


<#
    $Results.ResultCode:

    Return Value    Meaning
    0               Not Started
    1               In Progress
    2               Succeeded
    3               Succeeded With Errors
    4               Failed
    5               Aborted
#>



Saturday, July 30, 2011

Shadow Groups in Active Directory

Shadow Groups (SG) is an interesting topic. It’s not an actual type of group in the sense of security, distribution, global, domain local or universal, but rather a concept. A shadow group is a global security group which reflects the users found in a specific organizational unit (OU) in its group membership.

Shadow groups can be used for Fine Grained Password Policies, which can be applied to users (or inetOrgPerson objects) and global security groups only. Or if every user within a specific OU will require the same access to a resource, you can use shadow groups.

This means that you always want to keep the shadow groups up to date. If a new user is moved to the OU, it must also be added to the group. Likewise if a user is removed from the OU, it must also be removed from the group. There is no difference between a regular group and a shadow group in the sense that shadow groups bring any new or extra functionality when it comes to administration. These are just regular groups, but with a very specific purpose.

Keeping them up to date manually is really not an option, unless perhaps you know every single employee in the company personally and you are the only one managing AD. People aren’t machines, they make mistakes, it’s only human. Sooner or later, someone will forget to update the group when a user is moved between OUs.

This is when automation comes in handy. Anything that does not require human input should be automated.

Here we have a very simple criteria:
Users found in a specific OU should always be a member of a specific security group.
This does not require any human input or modification. We just need a way to compare the users in the OU with the users in the group and make any necessary changes.

Trust me when I say that there are many ways to do this. More than I will show you. I will limit this to the ds-tools and the PowerShell AD-cmdlets.

DS-Tools

The Quick and Dirty version:
dsquery user “<Organizational Unit distinguishedName>” –scope onelevel | dsmod group “<Shadow Group distinguishedName>” –chmbr

This will look for all users found in the specified OU, and limit the search to that OU only. Then it will clear the current group membership of the SG and add all users currently found in the OU.

The Clean and Clever batch file version:
Set OU=Organizational Unit distinguishedName (without quotes)
Set Group=Shadow Group distinguishedName (without quotes)

dsget group %Group% –members | find /v /i “%OU%” | dsmod group “%Group%” –rmmbr
dsquery * “%OU%” –filter “(&(sAMAccountType=805306368)(!memberOf=%Group%))” –scope onelevel | dsmod “%Group%” –addmbr


This will look at the group membership, pipe it to the find command, to find only the users where the OU’s distinguishedName is NOT present, and then pipe it to dsmod group to remove those users from the group. The next step is to look for all users in the specified OU that are NOT member of the Shadow Group already. It will then add any users found to the group.

PowerShell

Windows Server 2008 R2 with Active Directory cmdlets:
$OU=”Organizational Unit distinguishedName”
$Group=”Shadow Group distinguishedName”

Get-ADGroupMember –Identity $Group | Where-Object {$_.distinguishedName –NotMatch $OU} | ForEach-Object {Remove-ADPrincipalGroupMembership –Identity $_ –MemberOf $Group –Confirm:$false}
Get-ADUser –SearchBase $OU –SearchScope OneLevel –LDAPFilter “(!memberOf=$Group)” | ForEach-Object {Add-ADPrincipalGroupMembership –Identity $_ –MemberOf $Group}

This will do the same thing as the ds-tools clean and clever version, except it’s done in PowerShell with the AD cmdlets.

Once you’ve decided for what approach you want to take, you can easily create a scheduled task for this and ensure that the batch or PowerShell script runs at intervals that suits your organization. Just make sure that the user account the scheduled task runs under has got the proper privileges (such as log on as batch job and permission to update the Shadow Groups (write members) in Active Directory).

Thursday, July 28, 2011

The last VBScript you will ever need?

One of the first things people notice when writing their very first PowerShell scripts is that clicking the script file won’t run it, but instead open up Notepad. Which is nice, if you want to open up Notepad to read the script.
But I’m guessing that’s not why you clicked it.

There are of course several different ways to launch a PowerShell script. One is to run powershell.exe and specify the script as one of the arguments. This is useful if you want to run the script from a batch file, a shortcut, a scheduled task or wherever else you can specify a program to run.

I figured it would be neat to make the PowerShell script “clickable” by creating a shortcut to it and tell it to be run by powershell.exe. But doing this by hand is tedious work! (And as we all know, tedious work is the very reason you started scripting in the first place.)

Enter: VBScript

It’s easy to create a shortcut using VBScript, and it’s piece of cake to drag and drop a file onto another. –Which is something VBScript gladly accepts, taking the dropped file’s path as an argument to pass to the script. Knowing this, we can put two and two together, and write a script which allows you to drop a .ps1 file onto it, and then have it create a shortcut for us!

Now, this is a very straight forward script, which takes one file and creates a shortcut to it, adding the necessary parameters. It can be modified to allow you to drop several files onto it, or to run powershell.exe with additional parameters (such as setting execution policy).

Option Explicit

If WScript.Arguments.Count = 0 Then
    WScript.Echo "Drag and drop a .ps1 file onto the script."
    WScript.Quit
End If

Dim strPowerShellScriptFullPath : strPowerShellScriptFullPath = WScript.Arguments.Item(0)
Dim objFSO : Set objFSO = CreateObject("Scripting.FileSystemObject")

If Not objFSO.FileExists(strPowerShellScriptFullPath) Then
    WScript.Quit
End If
If Not LCase(objFSO.GetExtensionName(strPowerShellScriptFullPath)) = "ps1" Then
    WScript.Quit
End If

Dim strPowerShellScriptPath : strPowerShellScriptPath = Left(strPowerShellScriptFullPath, InStrRev(strPowerShellScriptFullPath, "\"))
Dim strPowerShellScript : strPowerShellScript = Mid(strPowerShellScriptFullPath, InStrRev(strPowerShellScriptFullPath, "\"))
Dim objShell : Set objShell = CreateObject("WScript.Shell")
Dim objShortCut : Set objShortCut = objShell.CreateShortcut(strPowerShellScriptPath & strPowerShellScript & " - Shortcut.lnk")

objShortCut.TargetPath = "%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe"
objShortCut.Arguments = "-NoExit -File """ & strPowerShellScriptFullPath & """"
objShortCut.WorkingDirectory = "%HOMEDRIVE%%HOMEPATH%"
objShortCut.IconLocation = "%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe, 0"
objShortCut.Description = "Run PowerShell script"
objShortCut.Save

I named my script “Create PowerShell Shortcut.vbs”.
And as always, there’s a very real risk of lines wrapping where they shouldn’t. If anything other than a .ps1 file is dropped onto it, nothing will happen (not even a message, in the above example).

When a ps1 file is dropped onto it, the script will create a shortcut to powershell.exe and assign the ps1 file path as an argument. The shortcut will be created in the same location as the ps1 file was dragged from. The VBScript and ps1 file does not have to be in the same location.


(It can of course also be run from the command line, as long as you pass a proper path, but that sort of defeats the purpose of it, now doesn’t it..)

Sunday, May 22, 2011

PowerShell, Active Directory and Expiring Passwords

Have you ever wanted to find out how many days are left until your (or someone else’s) Active Directory user account password expires? This (and just about everything else) can be done using Windows PowerShell.

Windows Server 2008 introduced a new Active Directory attribute called “msDS-UserPasswordExpiryTimeComputed”. This is a constructed attribute, which keeps track of when the password expires. Before Fine Grained Password Policies (FGPP) it used to be a simple matter of comparing the user’s pwdLastSet attribute with today’s date and subtracting it from the domain’s pwdMaxAge attribute.

I say used to be simple, because now you have to take FGPP into account. But as it turns out, it’s actually easier now, thanks to the new attribute.

Here comes the command. It looks a bit daunting at first, but broken down it’s actually quite simple.

PowerShell with the AD cmdlets:
(([datetime]::FromFileTime((Get-ADUser -Identity AHultgren -Properties "msDS-UserPasswordExpiryTimeComputed")."msDS-UserPasswordExpiryTimeComputed"))-(Get-Date)).Days

What we do here is begin with the Get-ADUser cmdlet followed by the user name, in this case AHultgren: Get-ADUser –Identity AHultgren
We also need to get the property containing the password expiration date. We do that by using the –Properties parameter and specify "msDS-UserPasswordExpiryTimeComputed".

Now if we wrap that entire command within parenthesis, we can return just the value of the attribute we need:
(Get-ADUser –Identity AHultgren –Properties "msDS-UserPasswordExpiryTimeComputed").”msDS-UserPasswordExpiryTimeComputed”

In my case the number 129538573151710728 is returned. This is not very helpful, so we need to translate the number to an actual date. We can do this by pre-fixing the command with [datetime]::FromFileTime() to convert it to a proper date:
[datetime]::FromFileTime((Get-ADUser -Identity AHultgren -Properties "msDS-UserPasswordExpiryTimeComputed")."msDS-UserPasswordExpiryTimeComputed")

This returns Wednesday, June 29, 2011 3:41:55 PM.

We could end it all here, since we now know the date the password will expire. But it can be interesting to know how many days are left. We do that by simply subtracting today’s date:
(([datetime]::FromFileTime((Get-ADUser –Identity AHultgren -Properties "msDS-UserPasswordExpiryTimeComputed")."msDS-UserPasswordExpiryTimeComputed"))-(Get-Date)).Days

Both the AD cmdlet and Get-Date must each be enclosed within parenthesis, and both commands must in turn also be enclosed within parenthesis, and then we can use “.Days” to just return the number of days. Instead of “.Days” you can pipe it all to Get-Member to show the available methods and properties you can use.

Running the final command looks like this:
PS C:\Windows\system32> (([datetime]::FromFileTime((Get-ADUser –Identity AHultgren -Properties "msDS-UserPasswordExpiryTimeComputed")."msDS-UserPasswordExpiryTimeComputed"))-(Get-Date)).Days
38

This means my password will expire in 38 days.


As an added bonus, this is how you turn a date into an Integer8 value:
(Get-Date(“2011-05-22”)).ToFileTime()
Which returns 129505176000000000.

Sunday, April 3, 2011

VBScript - WMI Ping

Sometimes it can come in handy to actually check, instead of just assume, that a destination is available before you let your script loose. One method you can use for this is ping. I recently wrote a log on script which pings a server before it attempts to map a network drive.

That script used a very simple, but yet effective, function that returned true or false based on the result. If ping failed (the function returned false), it would continue to try for 90 seconds before giving up. Here’s that little piece of code:

Function Ping(address)
    Ping = False
    Set objWMI = GetObject("winmgmts:\\.\root\cimv2")
    Set objPing = objWMI.Get("Win32_PingStatus.Address='" & address & "'")
    If objPing.StatusCode = 0 Then
        Ping = True
    End If
End Function

WScript.Echo Ping("google.com")

To test this function, save it in a text file with the file extension .vbs, and run the file. It should return either –1 (true) or 0 (false) to the screen.

But perhaps you want a bit more information than that, since the above script was not made for human readability, but rather to be used by other functions within the script. 

So I put together two more functions for this purpose. PingStatusCode and PingStatus. The former will get the return code and the latter will translate the code to text, when it is passed to it.

Function PingStatusCode(address)
    Set objWMI = GetObject("winmgmts:\\.\root\cimv2")
    Set objPing = objWMI.Get("Win32_PingStatus.Address='" & address & "'")
    PingStatusCode = objPing.StatusCode
End Function

Function PingStatus(PingStatusCode)
    Select Case Int(PingStatusCode)
        Case Int(0)
            PingStatus = "Success"
        Case Int(11001)
            PingStatus = "Buffer Too Small"
        Case Int(11002)
            PingStatus = "Destination Net Unreachable"
        Case Int(11003)
            PingStatus = "Destination Host Unreachable"
        Case Int(11004)
            PingStatus = "Destination Protocol Unreachable"
        Case Int(11005)
            PingStatus = "Destination Port Unreachable"
        Case Int(11006)
            PingStatus = "No Resources"
        Case Int(11007)
            PingStatus = "Bad Option"
        Case Int(11008)
            PingStatus = "Hardware Error"
        Case Int(11009)
            PingStatus = "Packet Too Big"
        Case Int(11010)
            PingStatus = "Request Timed Out"
        Case Int(11011)
            PingStatus = "Bad Request"
        Case Int(11012)
            PingStatus = "Bad Route"
        Case Int(11013)
            PingStatus = "TimeToLive Expired Transit"
        Case Int(11014)
            PingStatus = "TimeToLive Expired Reassembly"
        Case Int(11015)
            PingStatus = "Parameter Problem"
        Case Int(11016)
            PingStatus = "Source Quench"
        Case Int(11017)
            PingStatus = "Option Too Big"
        Case Int(11018)
            PingStatus = "Bad Destination"
        Case Int(11032)
            PingStatus = "Negotiating IPSEC"
        Case Int(11050)
            PingStatus = "General Failure"
        Case Else
            PingStatus = "Unknown Error"
    End Select
End Function

You can test the functions by adding these lines of code, outside the functions, before you run the script:

WScript.Echo PingStatusCode("google.com") & vbTab & PingStatus(PingStatusCode("google.com"))
WScript.Echo PingStatusCode("microsoft.com") & vbTab & PingStatus(PingStatusCode("microsoft.com"))

The script will first ping google.com and give you the return code and message, then it will ping microsoft.com and, again, give you the return code and message.

 

Here’s the MSDN reference for the Win32_PingStatus Class:
http://msdn.microsoft.com/en-us/library/aa394350(v=VS.85).aspx

Wednesday, March 23, 2011

PowerShell - Enable Exchange 2010 Mailbox from Active Directory

I installed Windows Live Writer the other day, so I figured I’d finally give it a shot. Step one was to load up with four cheese sandwiches, a cup of tea and a script! Earlier today when I pondered what to write about, I was deciding between managing NTFS inheritance with PowerShell or using redircmp to better control new domain computers.

Turns out I won’t be writing about either. In this post.

Lets instead turn our focus to Active Directory (AD) and Exchange 2010. Anyone who has worked with AD and Exchange before 2007 will most likely be familiar with the additional tabs in the Active Directory Users and Computers properties sheet for managing Exchange settings. These are gone in Exchange 2010, and as far as I know, won’t be coming back. All the Exchange management is now done in the Exchange Management Console (EMC). While EMC does allow you to create AD objects and mail/mailbox enable them, I find it a but cumbersome if all you want to do is create a mailbox for a user account.

One really nice touch with EMC is that it provides you with the PowerShell (PS) command to do <whatever it is you’re configuring>. Mailbox enabling a user account for example. This means if you’ve done it once in EMC you can easily do it in PowerShell instead. If you don’t mind typing or firing up a PS script to do this, it’s all good. But that’s not why we’re here.

Lets think back to the Exchange 2003 days and how we used to mailbox enable users by right clicking and going to “Exchange Tasks” and then selected what we wanted to do, and went through the wizard.

This is more like it!

Is there something similar in Exchange 2010? No. Not as far as I know (feel free to correct me). But if there’s a will, there’s a way! Since we already have the PowerShell command to mailbox enable the user, we can take advantage of the AD display specifiers and add our own menu item that will appear when you right click a user account. All it will take is a little bit of VBScript magic and an update of a configuration attribute in Active Directory.

We’ll start out by writing a VBScript. The script will perform a couple of checks to make sure that the user account is enabled (it’s a requirement for the PowerShell command), and that it isn’t already mail or mailbox enabled. Then it will proceed by launching PowerShell and finally execute the “Enable-Mailbox” cmdlet.

When a context menu item is selected, the object (ADsPath) itself is passed as an argument to the program.

Enable-Mailbox.vbs

Option Explicit
Dim objShell, objArgs, objUser

Set objShell = WScript.CreateObject("WScript.Shell")
Set objArgs = WScript.Arguments
Set objUser = GetObject(objArgs(0))

If Not IsEmpty(objUser.legacyExchangeDN) Then
    MsgBox "Object " & objUser.CN & " is already mail or mailbox enabled.", vbInformation, "Active Directory Domain Services"
    WScript.Quit
End If

If objUser.AccountDisabled = True Then
    MsgBox "Object " & objUser.CN & " is disabled. Please enable before proceeding.", vbInformation, "Active Directory Domain Services"
    WScript.Quit
End If

objShell.Run("powershell.exe -noexit Add-PSSnapin Microsoft.Exchange.Management.PowerShell.E2010; Enable-Mailbox -Identity '" & objUser.distinguishedName & "' -Alias '" & objUser.sAMAccountName & "'")

Set objArgs = Nothing
Set objUser = Nothing
Set objShell = Nothing

As always, watch out for unintentional line breaks.

Save this script and place it in a suitable location. I put mine in the \\testlab.local\NETLOGON share for high availability.

The next step is to edit the user account display specifier in the Active Directory configuration partition. I used ADSIEdit.msc to do this. Open it up and connect to the configuration naming context. The attribute is found in this location:
CN=user-Display,CN=409,CN=DisplaySpecifiers,CN=Configuration,DC=testlab,DC=local

At the top you will find an attribute called “adminContextMenu”. We have to edit this attribute to add our entry to the context menu. Leave any entry already there, and add a new one. Mine looks like this:
2,Enable Mailbox,\\testlab.local\NETLOGON\Enable-Mailbox.vbs

The first position is simply where in the menu it should appear. I only had one entry, so I picked number 2. The second position is the name you want to appear in the context menu. Prefixing a letter with the ampersand character (&) adds a shortcut to it. The third entry is the location of the script. In this case, Enable-Mailbox.vbs in the netlogon share.

This MSDN article talks a bit more about the adminContextMenu: http://msdn.microsoft.com/en-us/library/ms677915(v=vs.85).aspx

Once applied, start or restart the Active Directory Users and Computers mmc and look up a user without a mailbox. Right click the user and now “Enable Mailbox” should appear. If you click it, PowerShell will start and load “Microsoft.Exchange.Management.PowerShell.E2010” (this must be present for this to work), and then run the cmdlet to enable a mailbox for the user account!

This should work if the EMC is installed on the system. If you’re trying to do this remote, you may have to use a remote pssession to first connect to a suitable server. I haven’t tried this yet, since right now there are no clients in my lab. –Maybe I’ll revisit this later.

 

As always, try this out in a lab first to ensure it behaves as expected, before you go live.