Wednesday, December 7, 2011

Redeploy Group Policy Application

So you uninstall an application from a client machine and now want Active Directory to redeploy the application via Group Policy Software Installation. You don't want to redeploy the app domain wide so where do we go? The registry right? That would be correct but where and what keys/values? Microsoft recommends redeployment by deleting the respected subkey in:

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\AppMgmt

Here is the article from MS.

But here is an easier way to list those keys. PowerShell style!
Wildcards are allowed for the Parameter 'ApplicationName'

function Get-GPODeployedApp {

<# .SYNOPSIS Lists all of the group policy applied applications on the local computer. .DESCRIPTION Lists all of the keys under hklm:\software\microsoft\windows\currentversion\group policy\appmgmt This key contains all of the applied group policy software installations. .PARAMETER ApplicationName Filter on the GPO Name or the GPO Deployment Name .EXAMPLE PS C:\> Get-GPODeployedApp
Lists all of the applied GPO Software Installations.
PS C:\> Get-GPODeployedApp -ApplicationName Acrobat

param ([string]$ApplicationName = '')
# Get all keys for GPO Application installations
$keys = ls 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\AppMgmt' | Get-ItemProperty |
where { $($_."gpo name" -match $applicationName) -or $($_."Deployment Name" -match $applicationName) }

# Build custom object
foreach ($key in $keys) {
$myObject = New-Object -TypeName system.Object

$myObject | Add-Member -MemberType noteproperty -Name GPO_Name -Value $key."gpo name"
$myObject | Add-Member -MemberType noteproperty -Name DeploymentName -value $key."Deployment Name"
$myObject | Add-Member -MemberType noteproperty -Name GPO_ID -Value $key."gpo id"
$myObject | Add-Member -MemberType noteproperty -Name Path -Value $key.pspath



Ok so now we have listed all the subkeys. How do we remove them? Here is the function on how. It accepts pipeline input from the above function so you can do something like this:

Get-GPODeployedApp -ApplicationName Acrobat | Remove-GPODeployedApp

Remember, making registry changes require that you run the Remove-GPODeployedApp as Administrator with UAC turned on.

function Remove-GPODeployedApp {

<# .SYNOPSIS Remove GPO keys from registry for GPO application redeployment. Must be ran as Administrator. .DESCRIPTION Removes the required keys from the local registry so GPO installed applications can be redeployed. The cmdlet must be ran as Administrator. .PARAMETER Path The registry path to the key that is to be deleted. .EXAMPLE PS C:\> Remove-GPODeployedApp -Path 'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\AppMgmt\{a903040e-351f-4...'
This deletes the gpo key for any given application. Run gpupdate /force for GPO
PS C:\> Get-GPODeployedApp -ApplicationName Acrobat | Remove-GPODeployedApp
This filters all keys with Get-GPODeployedApp function and removes them with the
Remove-GPODeployedApp. Run gpupdate /force for GPO redeployment.
PS C:\> Get-GPODeployedApp | Remove-GPODeployedApp
This will remove all applied GPO application installation keys. Run gpupdate /force for GPO

[Parameter(Position=0, Mandatory=$true,ValueFromPipeline=$True,

process {
Remove-Item -Path $path -confirm
end {


Monday, June 6, 2011

Find a logged on users computer name

One thing that is hard to track down without 3rd party tools or something like SCCM is finding the computer name of a domain user that he/she is using or last used. And those that do even just a little help desk support get a sour taste in their mouth when they begin to ask the question, "Do you know what your computer name is"?

Well, with a little work up front you should be able to get this info a bit easier without asking the user for any information. What you have to enable is pretty simple:

Enabling 'Audit Account Logon Events' for your domain controller policy so that all your domain controllers will audit Successful, or failure, user logon attempts. Your can read more about this here. Once you have auditing enabled you might want to increase the size of your security event log size because the log begins to fill up very quickly, especially if your auditing more then just the Account logon events.

Once you have enabled Account Logon events all successful user requests for Kerberos tickets will be logged in the Security Log at event id: 4768. Now all we have to do is query the DC for this event and parse the event for the username, computer name and time it was requested. Now I know there are problems with this method. Such as, what if you have hundreds of domain controllers, how would you know what DC authenticated that user? Or that user has been logged into several other computers? Well one of those problems can be resolved if you have event log forwarding for this event on your domain to a centralized server, this can only be used if you have Windows Server 2008 however. Anyhow, here is the script.

function Get-UserComputerName {
<# .SYNOPSIS Searches a specified Domain Controller for the computername of a logged on user. .DESCRIPTION Queries a DC for Event ID 4768 (Kerberos authentication ticket,TGT) request from the servers Security event log. .PARAMETER UserName SamAccount name of the user to search for .EXAMPLE PS> .\Get-UserComputerName -UserName "John_Doe" -Server "My_DC"
Searches for user John_Doe on Domain Controller My_DC
PS> .\Get-UserComputerName -Username "John_Doe"
Searches for user John_Doe using the logged on server name for the current user
running the script.
PS> .\Get-UserComputerName
Searches the current user on the logged on server name

param([string]$username = $env:username,[string]$server = $env:logonserver)
$ErrorActionPreference = "silentlycontinue"
if ($server.StartsWith("\\")) {$server = $server.Remove(0,2)}

$events = Get-WinEvent -ComputerName $server -MaxEvents 5 -FilterHashTable @{logname="security";id=4768;data=$username}
# Check if error has been raised from EventLog Query.
if (!$?) {Write-Warning "No successful logon events were found on Server: $server for Username: $username"

foreach ($event in $events) {
$myObject = New-Object -TypeName system.Object
[string]$Computer = $event.message.split("`n") | Select-String "Client Address"
$addressLine = $computer.replace("Client Address:",'')
$addressLine = $addressLine.trim()
if ($addressLine.startswith("::ffff:")) { $address = $addressLine.replace("::ffff:",'') }
$DNSResult = [system.Net.Dns]::Resolve($address)
$ComputerName = $DNSResult.HostName
$timeStamp = $event.timecreated

$myObject | Add-Member -MemberType noteproperty -Name AuthDC -Value $server
$myObject | Add-Member -MemberType noteproperty -Name TimeStamp -Value $timeStamp
$myObject | Add-Member -MemberType noteproperty -Name UserName -Value $username
$myObject | Add-Member -MemberType noteproperty -Name IPAddress -Value $address
$myObject | Add-Member -MemberType noteproperty -Name ComputerName -Value $computerName

Friday, May 6, 2011

Migrating from Windows XP to Windows 7 with USMT and Powershell!

If you've ever had to migrate a users (or perhaps your own) files/folders from one system to another you know how annoying and tedious this process is. To help with this Microsoft release a very useful tool called USMT (User state migration tool) with Windows XP many years ago that many people still use today. Today Windows 7 still supports USMT but just a later version; version 4 specifically.

When using the utility you need to pass parameters to either 'ScanState.exe' when scanning the system and 'LoadState.exe' when loading the data from your scanstate scan. If you've seen the parameters from Scanstate and Loadstate you know the options can be a bit confusing. To help with this I've created a script that makes this a bit easier.

Call the script with the 'mode' parameter first with ScanState. After your scan your old system run the script again on the new computer with the 'mode' parameter of LoadState.

You can only migrate from an XP system to Windows 7. Or a Win7 to another Win7. You can't migrate a system from XP to XP. If you need to do this then you will have to use a previous version of USMT.

Runs USMT 4.0 to migrate app/user/OS settings from XP to Win7
Runs USMT 4.0 to migrate app/user/OS settings from XP to Win7
USMT 4 will only migrate from XP to Vista OS and later. XP to XP is not supported.
Run as administrator
Pass either ScanState or LoadState to this parameter. Any other parameter passed will throw an error.
ScanState will scan the local machine and upload data to a network share.
LoadState will load the uploaded data that was created from ScanState and apply the settings on the local machine.
./usmt.ps1 -mode scanstate

Scan and upload the local machine settings to a network location. This is for the logged on user.
This must be ran first before 'loadstate' mode in order to create the migration store.
./usmt.ps1 -mode loadstate

Download and apply the settings from the data that was gathered using ScanState mode
./usmt.ps1 -mode scanstate -VerboseOff
./usmt.ps1 -mode loadstate -VerboseOff

Turns off Scanstate or Loadstate Verbose mode.



$VerboseOff = $false

if ($verboseOff) {$VerbosePreference="SilentlyContinue"} else {$VerbosePreference="Continue"}

$srcFolder = "\\myServer\users\usmt4"
$usmtFolder = "c:\usmt"
$storePath = "\\myServer\users\MigStore_$env:username"

function scanstate {
# If USMT folder exist, do not copy from network location
if (!(Test-Path $usmtFolder)) {
Write-Verbose "Copying data from $srcFolder to $usmtFolder"
copy $srcFolder $usmtFolder -Recurse

Set-Location $usmtFolder

# sets UMST Scanstate to migrate Docs,Apps and User settings. Also sets logging at the highest level and specifies to only
# migrate any users who are currently logged on. LocalOnly parameter is specified to omit removeable or network drive data.
$ScanStateCmd = ".\scanstate.exe $storePath /i:migdocs.xml /i:migapp.xml /i:miguser.xml /localonly /o /v:13 /uel:0 /c /l:scanstate.log"
Write-Verbose "Running command: $scanStateCmd"
Invoke-Expression $scanStateCmd

function loadstate {
# If USMT folder exist, do not copy from network location
if (!(Test-Path $usmtFolder)) {
Write-Verbose "Copying data from $srcFolder to $usmtFolder"
copy $srcFolder $usmtFolder -Recurse

Set-Location $usmtFolder

# Loads the user store from the network location
$LoadStateCmd = ".\loadstate.exe $storePath /i:migdocs.xml /i:migapp.xml /i:miguser.xml /v:13 /uel:0 /c /l:loadstate.log"
Write-Verbose "Running command: $LoadStateCmd"
Invoke-expression $LoadStateCmd

function Select-Item
<# .Synopsis Allows the user to select simple items, returns a number to indicate the selected item. .Description Produces a list on the screen with a caption followed by a message, the options are then displayed one after the other, and the user can one. Note that help text is not supported in this version. .Example PS> select-item -Caption "Configuring RemoteDesktop" -Message "Do you want to: " -choice "&Disable Remote Desktop",
"&Enable Remote Desktop","&Cancel" -default 1
Will display the following
Configuring RemoteDesktop
Do you want to:
[D] Disable Remote Desktop [E] Enable Remote Desktop [C] Cancel [?] Help (default is "E"):
.Parameter Choicelist
An array of strings, each one is possible choice. The hot key in each choice must be prefixed with an & sign
.Parameter Default
The zero based item in the array which will be the default choice if the user hits enter.
.Parameter Caption
The First line of text displayed
.Parameter Message
The Second line of text displayed

param( [String[]]$choiceList,
[String]$Caption = "Please make a selection",
[String]$Message = "Choices are presented below",
[int]$default = 0
$choicedesc = New-Object System.Collections.ObjectModel.Collection[System.Management.Automation.Host.ChoiceDescription]
$choiceList | foreach { $choicedesc.Add((New-Object "System.Management.Automation.Host.ChoiceDescription" -ArgumentList $_))}
$Host.ui.PromptForChoice($caption, $message, $choicedesc, $default)

switch ($mode) {
"scanstate" {
if (Test-Path $storePath) {
$choice = [bool](Select-Item -Caption "The path $storePath exist! Do you want to overwrite?" `
-Message "Press 'Y' or 'N' ?" -choiceList "&No","&Yes" -default 0 )

if ($choice) {
Write-Verbose "Overwriting folder: $storePath"
} else {
Write-warning "Overwrite skipped. Script exiting..."
} else {
"loadstate" {
if (Test-Path $storePath) {
} else {
Write-warning "User store for $env:username cannot be found at $storePath `nRun script in 'ScanState' mode first."
Write-warning "Script exiting..."

This script will create a migration store with the logged on user to a network location that you specify in the $storepath variable. USMT 4 supports hardlink migrations to speed up the transfer and reduce the migration size. I've not included this in this script.

Credit for the Select-Item function goes to James O'Neill.

Thursday, April 28, 2011

2011 Scripting Games Certificate

Funny how I was awarded with this after the Scripting Games. Looking forward to next years games.

Thursday, April 21, 2011

Scripting Games Beg6

Here is my posting of Game 6 of the 2011 Scripting Games. The requirements were to write a script that would search the WindowsUpdate.log file for any Fatal errors. Bonus points were awarded for brevity. If aliases were going to be used an explanation of the alias was needed.
# Alias: gc = Get-Content
# sls = Select-String
# also using the Environment variable to find the WindowsUpdate.log file

gc $env:systemroot\windowsupdate.log | sls "Fatal"
The Get-Content command is pretty simple. It prints the file specified by its default argument. I shortened the code by using the $env variable to access the systemroot variable path. If your not too familiar with the Environment Provider you can enter the following command to see whats available thru it.

cd env:

On my system "SystemRoot" goes to C:\Windows. So $env:systemroot\windowsupdate.log resolves to:


I then pipe the output of the log file to select-string cmdlet and search for any text called "Fatal".
This was probably the easiest of all the games and really didn't take much time to come up with this solution. This challenge was probably the biggest reason why I regret not entering the Advanced Games. 8)

Wednesday, April 20, 2011

Scripting Games Beg5

function Get-BasicOSinfo {

Gets basic Compuer OS Info
Gets basic OS Info from local or remote machines
.Parameter ComputerName
Name of the Computer (local or remote).

PS> get-BasicOSInfo -computername MycomputerName
PS> "computername" | get-BasicOSinfo
PS> get-content input.txt | get-BasicOSInfo
PS> import-csv input.csv | get-BasicOSInfo


.Parameter ComputerName
Computername as a string to query


ValueFromPipeline=$true,ValueFromPipeLineByPropertyName= $true)]
begin { $date = Get-Date -format MM/dd/yy } # Gather current date

process {
# Query WMI of local or remote system
$ComputerSystem = gwmi Win32_Computersystem -Computer $computername
$OperatingSystem = gwmi win32_operatingsystem -ComputerName $computername
$OS = $operatingSystem.caption

$user = $(($computerSystem.username.split("\"))[1])
$computer = $computersystem.caption
$domain = $computersystem.domain

# Populate table with tabs and newline/Carriage Return charaters
# then ouput to file in ascii format.

$result += "User:`t`t $user`r`n"
$result += "Computer:`t $computer`r`n"
$result += "Domain:`t`t $domain`r`n"
$result += "OS:`t`t $OS`r`n"
$result += "User information collected on:`t $date`r`n"
$result += "`r`n"
$result | Out-File result.txt -Encoding ascii


Wednesday, May 20, 2009

PSCX 1.2 Beta Released

PowerShell Community Extensions 1.2 have been released in Beta form. You can find it here.

If you an unfamiliar with the PSCX tools it's WELL worth the look. Unbelievably helpful stuff!

Tuesday, May 12, 2009

Run Powershell V.1 & 2 on the same machine

Powershell MVP Shay Levy has a really cool article on running Powershell Version 1 and 2 on the same computer using Virtual PC on Windows 7.

A really interesting read. Check it out here.

Thursday, May 7, 2009

PowerShell Plus 3.0 now released!

If you have a PowerShell editor then you probably have heard of Idera's product PowerShell Plus. If you havent you can find more info here. They also offer tools for the SQL and Sharepoint Admin so its worth the look if your one of those guys! 8)

I have been using PS+ version 2 for quite some time now and I absolutely love it. IMHO it is hands down the best PS text editor and console application for PS. It has an excellent debugger, Intellisense code completion, variable previewer, and other visual options to fit pretty much anyones needs or wants. But PS+ isn't just for PS files although that is it's main function. PS+ also has language support for XML, HTML, C#, VB.Net, Vbscript, and the newly PowerShell V2 PowerShell Modules.

Several improvement have been included in the V3 release such as support for VBscript and Windows 64bit versions (If your going to run PS+ on a 64bit machine vbscript support will not be available as the PS+ dependancy only works with a 32bit dll "tlbinf32.dll"). You can now download and upload scripts directly from or using the editor. Very cool! The most in your face upgrade the new Intellisense Help window.

Here you can see without using Get-help the data type for the given parameter and whether it is an optional parameter, a switch parameter (indicated by the 'S' or number of the left of parameter name). What I like the most about this is it gives you the same info for that given parameter that you would receive from Get-help, saving you tons of time in the long run. In addition you can click on the bottom available links in the help windows to either do a google search on your cmdlet or paramater or if available visit MSDN directly.


Great work Tobias and the rest of the people at Idera.

Wednesday, March 18, 2009

Random Password Generator

Here I'm using a VERY simple function to generate a random password. The function is mainly built around the use of the new V2 CTP cmdlet called Get-Random. This cmdlet takes all the headache out of using the code like this:

$r = New-Object system.Random

I know, it's not a lot of typing but with the Get-Random cmdlet really is much easier to generate random numbers, plus it has the ability of choosing random items from any given array from the pipeline.

This code is VERY simple compared to some of those password scripts out there.

function Get-RandomPassword {
param([int]$length = 4)
@(1..$length | % { [char](Get-Random -Minimum 48 -Maximum 122) }) -join ''

Here, I'm simply grabbing a set number of random ascii characters and using the new join operator to join the characters to spit out the random password.

All that is needed now is to add this to your profile or to a module; import and your ready to start pumping passwords through. Pretty easy eh!

I will post later with the Advanced Function version of this so it can be used in the pipeline and with built-in help features.