Saturday, July 05, 2008

TFS Deployer Least Privilege

The original TFS Deployer attempted to access various restricted resources than effectively required it to run as an administrator user. Since some changes were made in February, Deployer can now be executed under a standard user account with some additional considerations which follow.

Team Foundation Server Permissions

The TFS Deployer service account must be a member of the Team Foundation Valid Users server-level group on the server specified by the TeamFoundationServerUrl config setting and also a member of the Readers project-level group for the Team Projects that Deployer will be monitoring.

If you intend to make use of the Retain Build feature, the Deployer service account will also need the Write To Operational Build Store project-level permission on applicable Team Projects.

PowerShell Execution Policy

TFS Deployer can execute Windows Command Scripts if you really need it to but PowerShell scripts are the preferred option. However, PowerShell is built to be secure-by-default and installs with script-running capabilities disabled. It is left for the user to enable scripts after PowerShell has been installed and can be performed by opening the PowerShell console as an administrator and executing either:

Set-ExecutionPolicy AllSigned ; # or
Set-ExecutionPolicy RemoteSigned

The former choice will require you to sign all your deployment scripts in source control with a certificate trusted by the Deployer. Scott Hanselman has a comprehensive post on his blog describing the signing process. The latter choice will not impose this requirement because files retrieved from source control by Deployer are not marked with a zone identifier.

HTTP Namespace Reservation

TFS Deployer utilises self-hosted WCF over HTTP to subscribe to Build Quality Change events from TFS. This technique relies on the Windows HTTP Server API which requires the Deployer service account to have been granted rights to listen on the portion of the HTTP URL namespace it uses for event notification. By default only local administrators have access to the entire namespace and permissions must be granted using the httpcfg tool (on Windows XP and Server 2003) or the netsh tool (on Vista and Server 2008).

Using the netsh tool, from an administrator command prompt, the following command (as a single line) will grant rights to the appropriate URL for TFS Deployer (assuming you are using the default port number):

netsh http add urlacl url=http://+:8881/BuildStatusChangeEvent user=DOMAIN\TfsDeployerUser

Using the httpcfg tool is more difficult as it requires an ACL string in Security Descriptor Definition Language (SDDL). However, Dominick Baier has created a tool to simplify this process and I can recommend it.

Additional Permissions

All deployment scripts executed by TFS Deployer will be executed under the context of the service account unless the scripts explicitly contain alternate credentials. If the scripts copy files to a network share, configure a web site in IIS, or even upgrade the schema of a SQL database, ensure that the service account also has the minimum privileges to perform those actions too.

Any problems, questions, or suggestions... let me know.

 Sunday, April 13, 2008

TFPT TreeClean tamed with PowerShell

Update: Philip Kelley from Microsoft, creator of TFPT, has kindly informed me that the July 2008 release of the TFS Power Tools is now available for download. This new version includes enhancements to TFPT TreeClean that allow you to specify which files to include or exclude and as such solves the main problem my TreeClean PowerShell script was created for. The output format of the new TreeClean also renders this script incompatible but the general concepts used by the script may still be useful.


I like the Team Foundation Server 2008 Power Tools, there are some great additions in there. One particular utility, TreeClean, has a great concept but is a little overzealous for my tastes.

The purpose of TreeClean is to find all local files in your workspace folders that do not exist in source control and then allow you to delete all of them. The problem is that it includes *.user files in its find results and the delete option is all or nothing. The list of files can also be rather overwhelming.

Thankfully we can get some more control by piping the results through PowerShell, starting with a simple script like this:

$ProgFiles = $Env:ProgramFiles ;
$ProgFiles32 = (Get-Item "Env:ProgramFiles(x86)").Value ;
if (-not [String]::IsNullOrEmpty($ProgFiles32)) { $ProgFiles = $ProgFiles32 ; }

$TFPTEXE = Join-Path -Path $ProgFiles `
    -ChildPath "Microsoft Team Foundation Server 2008 Power Tools\TFPT.exe" ;
if (-not (Test-Path -Path $TFPTEXE)) { throw "TFPT.EXE not found." ; }

[string]$Root = Resolve-Path -Path (Get-Location) ;

& $TFPTEXE treeclean `
    | Where-Object { $_ -like ($Root + "*") } `
    | Get-Item -Force ;

Once we have this script saved we can get more information from the results. For example, we can get count and list rogue files by extension:

TreeClean.ps1 | group Extension

We can exclude directories:

TreeClean.ps1 | ? { -not $_.PSIsContainer }

And finally we can delete everything but *.user files:

TreeClean.ps1 | ? { $_.Extension -ne ".user" } | Remove-Item

Now I can clean all the junk from my workspace but keep all my user-level project settings. However, while sorting through the extension-grouped report, looking for files to check-in before cleaning, there was a lot of noise from the build outputs. My quick solution:

gci -inc *.sln -rec | % { MSBuild /t:Clean $_ }

It also has the nice side effect of significantly reducing your workspace folder size if you want to zip it up and send it somewhere.

 Tuesday, October 16, 2007

PowerShell Resources

Ever since I heard of the concept of PowerShell (or Monad as it was known then) I was excited. Now that is been RTM for some time and I have had an opportunity to work with it in a production environment I love it even more.

While PowerShell could be summarised as a cross between a *nix shell and the .NET Framework there is still a lot unique to PowerShell alone and learning how it works and finding efficient tools to work with it is still necessary to make the most of PowerShell.

To learn PowerShell I purchased Bruce Payette's book Windows PowerShell In Action. It is a good-size detailed book at over 500 pages and receiving both the soft-cover book and the searchable PDF was excellent value. Pretty much every aspect of PowerShell is described including why certain design decisions were made. My only issue with the book, and it's not a big issue, is that I was itching to write some PowerShell scripts but script files aren't explained until Chapter 8 and security for script files isn't fully explained until Chapter 13 (the last chapter).

When I started to write my scripts I had the PowerShell console open on one monitor and Notepad open on the other. I would try certain commands in the console window and when they worked and gave the results I wanted I would copy them to the script in Notepad, save, and switch back to the console window to test the script. I still pretty much work like this today but I've replaced Notepad.

Considering I spend most of my time in Visual Studio I really wanted the same Intellisense and Syntax Highlighting experience when writing PowerShell scripts. The first PowerShell "IDE" I encountered was PowerShell Analyzer but I felt overwhelmed by the UI given that all I wanted was to edit .ps1 files. It seems very capable but just didn't feel right. More recently I have tried PowerGUI and it is very close to "Notepad for PowerShell". I have used it to develop the scripts for the last two PowerShell posts and I recommend it.

Do you have any resources you feel have been invaluable for getting the most out of PowerShell?

 Sunday, October 14, 2007

Report Services Automation With PowerShell

In late September Paul Stovell wrote about a set of VB.NET scripts he prepared to help deploy reports to SQL Server Reporting Services. If you've ever had the displeasure of deploying SSRS reports without Visual Studio then you'll understand how much it sucks.

Paul went to the effort to write individual scripts for creating folders and data sources on the server and uploading report definitions and configuring permissions. With Paul's work simple command scripts can then be used deploy reports.

However these command scripts still need to be written and they end up containing much of the same information as can be found in the .rptproj project file and the .rds data source files. I despise the idea of maintaining any sort of configuration information in more than one place so adding to the deploy command script whenever I add a report to the project in Visual Studio just makes me cringe.

Additionally, as Paul briefly mentions, MSBuild (and therefore Team Build) does not support Report Services projects so, once again, to deploy your reports as part of Continuous Integration you need to have separate tools.

Today I constructed a lengthy PowerShell script to take a Report Services .rptproj project file and output a command script that utilises Paul's VB.NET scripts to deploy the reports as per the project settings. Due to the size of the script rather than publishing it inline, you can download it here.

The script accepts three parameters. ProjectFile is the path to .rptproj file for the reports you want to deploy. If you omit this parameter the script uses the first report project file it finds in the current directory. The second parameter, ConfigurationName tells the script which project configuration to use for the target server URL and destination folders. If you omit this parameter the script uses the first configuration defined in the project. The last parameter SearchPaths is a list of paths for the script to search when locating both rs.exe and Paul's .rss files. The SearchPaths parameter is automatically combined with the environment PATH variable and may be omitted.

Here is an example usage:

PS C:\Users\Jason\Dev\MyReports> .\Deploy-SqlReports.ps1 `
    -ProjectName MyReports.rptproj `
    -ConfigurationName Release `
    -SearchPaths "C:\Tools\Report Services\" `
    | Out-File deploy.cmd -Encoding ASCII;

As always, my PowerShell skills are slowly improving and this script is not necessarily perfect in either robustness or efficient use of PowerShell. Hopefully it will be as useful to you as it has been to me and any changes you need should be easily made. Please leave a comment with your thoughts and suggestions.

 Saturday, October 13, 2007

Find Duplicate Files With PowerShell

I have pieced together a simple PowerShell script to recursively locate all duplicate files (by content, not name) below a chosen directory. It is not the most elegant code but for my purposes it works and hopefully you will be able to tweak it to suit your needs.

Firstly, it filters out any zero-length files. Zero-length files are naturally duplicates of each other and can be found quite trivially without my script. Secondly it groups all files by their length because if the length doesn't match, they can't have the same content. The script then excludes the length-groups with only one entry and calculates the MD5 hash of the remaining files. Groups of files with both matching size and hash are then returned in the results.

The hashing function was taken from the Duplicate Files post on the Windows PowerShell team blog. It simply uses the .NET cryptography namespace to compute the hash. From here you could easily exchange the MD5 algorithm for SHA1 or any other preferred algorithm.

Due to the need to read the entire contents of potentially matching files to compute the hash this can cause the script to take a long time against larger files. Executing the script against deep directory structures with many files will take longer too. The script could be easily modified to take a filtered input of files to only find, for example, duplicate photos.

Here is the script:

param ([string] $Path = (Get-Location))

function Get-MD5([System.IO.FileInfo] $file = $(throw 'Usage: Get-MD5 [System.IO.FileInfo]'))
{
    # This Get-MD5 function sourced from:
    # http://blogs.msdn.com/powershell/archive/2006/04/25/583225.aspx
    $stream = $null;
    $cryptoServiceProvider = [System.Security.Cryptography.MD5CryptoServiceProvider];
    $hashAlgorithm = new-object $cryptoServiceProvider
    $stream = $file.OpenRead();
    $hashByteArray = $hashAlgorithm.ComputeHash($stream);
    $stream.Close();

    ## We have to be sure that we close the file stream if any exceptions are thrown.
    trap
    {
        if ($stream -ne $null) { $stream.Close(); }
        break;
    }

    return [string]$hashByteArray;
}

$fileGroups = Get-ChildItem $Path -Recurse `
    | Where-Object { $_.Length -gt 0 } `
    | Group-Object Length `
    | Where-Object { $_.Count -gt 1 };

foreach ($fileGroup in $fileGroups)
{
    foreach ($file in $fileGroup.Group)
    {
        Add-Member NoteProperty ContentHash (Get-MD5 $file) -InputObject $file;
    }

    $fileGroup.Group `
        | Group-Object ContentHash `
        | Where-Object { $_.Count -gt 1 };
}

Once you have the output of the script you could use it delete the unnecessary files:

$dupes = Get-DuplicateItems;
$dupes | % { ($null, $rest) = $_.Group; $rest; } `
| Remove-Item -WhatIf;

As always, if you have any suggestions or improvements don't hesitate to leave a comment here.

 Saturday, September 15, 2007

PowerShell Vulnerability

Some time ago I put together a template for a file that can be interpreted as both a batch command script for cmd.exe and a SQL script for SQL Server Management Studio and sqlcmd.exe. It works really well for enabling someone unfamiliar with SQL Server to deploy database update scripts.

I've been working with PowerShell quite a lot lately and I was wondering if it would be possible to have a file that would be interpreted by both cmd.exe and PowerShell.exe. It turns out that Jay Bazuzi, a developer at Microsoft, has already found an elegant solution. However, upon inspecting the code I became concerned how it would be effected by PowerShell's default setting to disable all scripts and especially disable unsigned scripts from remote sources.

I have since worked with Jay's sample on my home machine and discovered that it effectively bypasses PowerShell's anti-script security by piping the commands to it's interactive mode. As a result, it was relatively trivial to write a batch command script that would enable PowerShell to run all future scripts and modify the user's PowerShell profile to ensure it stays that way.

Thankfully, security in the OS and other applications is making it harder to get a command script on to another computer and get the user to run it but with the capabilities of PowerShell potentially available for malicious use it's just one more reason to not run as an administrator and minimise the damage.

 Wednesday, September 12, 2007

PowerShell Regular Expression Compiler

As an exercise in the mostly useless, I decided it would be interesting to try my hand at writing a PowerShell script to compile a regular expression pattern to a .NET assembly. I sure someone has already created a NAnt or MSBuild task to do this but I felt this would be a good way to increase my familiarity with PowerShell.

The script requires the RegEx pattern and an output type name and full namespace to work. You can optionally pass an AssemblyName but if omitted the type and namespace will be used to form the output file name. You can also specify the -ignoreCase or -multiLine switches to enable that behaviour on your expression. There are other options that probably should be supported but they can be easily added if you actually have a use for this. Without further delay, here is the code:

param
(
    [string] $pattern = "",
    [string] $typeName = "",
    [string] $fullNamespace = "",
    [System.Reflection.AssemblyName] $assemblyName = $null,
    [switch] $ignoreCase,
    [switch] $multiLine
)

if ($pattern -eq "") { throw ("-pattern required"); }
if ($typeName -eq "") { throw ("-typeName required"); }
if ($fullNamespace -eq "") { throw ("-fullNamespace required"); }

if ($assemblyName -eq $null)
{
    $assemblyName = New-Object System.Reflection.AssemblyName `
        ($fullNamespace + "." + $typeName);
    $assemblyName.Version = New-Object System.Version (1, 0);
}

$sysRx = @{};
$sysRx.Namespace = "System.Text.RegularExpressions";
$sysRx.RegEx = [System.Text.RegularExpressions.RegEx];
$sysRx.RegExOptions = [System.Text.RegularExpressions.RegExOptions];

$options = $sysRx.RegExOptions::None;
if ($ignoreCase) { $options = $options -bor $sysRx.RegExOptions::IgnoreCase; }
if ($multiLine) { $options = $options -bor $sysRx.RegExOptions::Multiline; }

$info = New-Object ($sysRx.Namespace+".RegexCompilationInfo") `
    ($pattern, $options, $typeName, $fullNamespace, $true);

$popDir = [System.Environment]::CurrentDirectory;
[System.Environment]::CurrentDirectory = $PWD;
$sysRx.RegEx::CompileToAssembly($info, $assemblyName);
[System.Environment]::CurrentDirectory = $popDir;

Here is an example usage:

./Compile-RegEx.ps1 "\s+" "Spaces" "CodeAssassin.RegEx" -ignoreCase

 Thursday, September 06, 2007

My Manwich! PowerShell Which

One feature I've loved during my brief ventures into Linux-land is the which command, not to be confused with other tasty items. For those not familiar with the command, it enables you to determine the full path to an executable in your environment path that would be executed if only the name of the executable is entered at the command prompt.

This is useful in situations where you have different versions of the same executable on your file system (perhaps even a malicious version) or if you simply want to know where the executable resides. I have often wanted to perform a similar search on my Windows PCs but to the best of my knowledge, the standard command prompt does not provide an easy way to do this.

Gratefully, as is becoming the pattern lately, PowerShell makes this very simple. Simple to the point that I don't really need to bundle it into a function. For example, here is how I locate the SQL Server Management Studio executable that runs when I use Start, Run, "sqlwb":

($Env:Path).Split(";") | Get-ChildItem -filter sqlwb*

If you don't have SQL installed, you can switch the -filter parameter for "calc*" or similar. You can see that it isn't limited to just executable files on your path either and while it doesn't duplicate the default behaviour of the Linux which command, wrapping the one-liner into a function with some extra switches would get it very close.

 Monday, September 03, 2007

PowerCleaning, Part 2

Each day I live with wanting to make full use of excess storage space to keep a record of every file that has ever existed in all it's versions and the conflicting need to have a neat and tidy file system without any junk sitting around.

Recently, the desire to have a ClickOnce deployment folder with only the latest version in it has won. With only the most recently published version within, the folder is a much more manageable size for distributing to other deployment servers.

However, it can be tedious to ensure only the appropriate files are deleted when cleaning a deployment folder manually, and automating the process is infinitely better. I have put together a basic PowerShell function to handle the situation.

function Clean-ClickOnceApplication ([string] $Path = (Get-Location), [switch] $WhatIf = $false)
{
    ($current, $oldList) = Get-ChildItem -path (Join-Path $Path "*") -include "*.application" `
        | ? {$_.Name -match "_\d+_\d+_\d+_\d+\." } | sort LastWriteTime -desc;
    foreach ($old in $oldList) {
        $oldFolder = $old.Name.Substring(0, $old.Name.Length - $old.Extension.Length);
        Remove-Item (Join-Path $Path $oldFolder) -recurse -WhatIf:$WhatIf;
        Remove-Item $old -WhatIf:$WhatIf;
    }
}

Simply pass the ClickOnce deployment folder as the -Path parameter (or omit to assume the current folder) and optionally enable the -WhatIf switch to test which files and folders would have been deleted.

The script currently uses the time stamps of the .application files rather than the version numbers but unless someone has been messing with the digitally-signed files this shouldn't be a problem. If it really needs to be handled by version number, I have some ideas for handling that.

 Thursday, August 02, 2007

PowerShell Registry Find and Replace

Keys I recently encountered a server where SQL Server had somehow been installed to the admin user's mapped U: drive instead of drive C:. As a result all SQL file paths in the registry referred to "U:\Program Files\Microsoft SQL Server\..." but for most users (including the SQL service account) the U: drive did not map to C:. This prevented Management Studio from working and probably many other issues that weren't as visible.

I wanted a fast way to find all "U:\Program Files" references in the registry and repoint them to drive C:. The standard Windows regedit.exe only supports Find but not Replace (and there were a lot of keys to fix) and third party registry tools available on the Internet fall into the untrustworthy category for fixing servers.

I ended up writing a quick C# console app to perform the job.The C# app was able to solve the problem and the server works properly now but I felt there should be an easier way: PowerShell.

I've spent an evening hammering out a basic pair of find and replace functions for PowerShell. They don't make as much use of PowerShell's declarative pipelined nature as I'd like but they work well. The replace function is particular dangerous if you misuse it so be careful. Perhaps I will implement the -WhatIf switch some day.

The find function is simply named Find-RegistryValue. At the moment the function only looks in values, not keys or value names because these are already quite easy to search on with basic PowerShell one-liners. As input the function expects a "seek" parameter being the text sought and optionally a path to a registry key to begin searching from. If the "regpath" is not provided it defaults to Get-Location and if it is not a registry path it throws.

The find function will return an array of Hashtable objects with all the information you should require: the RegistryKey, the name of the value in the key, and the value itself containing the sought text. The code follows:

function Find-RegistryValue (
    [string] $seek = $(throw "seek required."),
    [System.Management.Automation.PathInfo] $regpath = (Get-Location) ) {

    if ($regpath.Provider.Name -ne "Registry") { throw "regpath required." }

    $keys = @(Get-Item $regpath -ErrorAction SilentlyContinue) `
        + @(Get-ChildItem -recurse $regpath -ErrorAction SilentlyContinue);

    $results = @();

    foreach ($key in $keys) {
        foreach ($vname in $key.GetValueNames()) {
            $val = $key.GetValue($vname);
            if ($val -match $seek) {
                $r = @{};
                $r.Key = $key;
                $r.ValueName = $vname;
                $r.Value = $val;
                $results += $r;
            }
        }
    }

    $results;
}

The replace function is named Replace-RegistryValue and relies on the find function to work, resulting in very similar behaviour. It requires the text sought and the registry path just like the find function but it also requires the "swap" parameter which is the text to replace the sought value with. It calls the find function itself and uses the output to first promote the key to a writable instance then replace the value and return the results. The results include the RegistryKey, the name of the value in the key, the old value and also the new value. Here is the code:

function Replace-RegistryValue (
    [string] $seek = $(throw "seek required."),
    [string] $swap = $(throw "swap required."),
    [System.Management.Automation.PathInfo] $regpath = (Get-Location) ) {

    $find = Find-RegistryValue -seek $seek -regpath $regpath;
    $results = @();

    foreach ($target in $find) {
        $nval = $target.Value -replace $seek, $swap;
        $r = @{};
        $r.Key = $target.Key;
        $r.ValueName = $target.ValueName;
        $r.OldValue = $target.Value;
        $r.NewValue = $nval;
        $results += $r;
        $wKey = (Get-Item $r.Key.PSParentPath).OpenSubKey($r.Key.PSChildName, "True");
        $wKey.SetValue($target.ValueName, $nval);
    }

    $results;
}

If you have any suggestions for improving the code or perhaps even a better naming convention for the pair, please leave a comment.

 Wednesday, July 25, 2007

Touched By PowerShell

Today I was asked how to "touch" all files in a folder in Windows to set the Modified timestamp to now. If I was asked about the Created timestamp I would have suggested to copy the files to a new folder and use the copies which will have a new Created timestamp. Unfortunately Windows doesn't seem to have an easy way to edit the Modified timestamp.

Thankfully, everyone who has Windows also has PowerShell, or should have, or should upgrade their OS. With PowerShell, touching files is easy:

Get-ChildItem * | % { $_.LastWriteTime = [DateTime]::Now }

 Friday, June 22, 2007

Drive By PowerShelling

I have been using an external USB hard drive to take backup copies of certain files on several machines. Each machine has different drive configurations though so the USB drive will be assigned different drive letters on each machine.

I could have found an unused drive letter common to all the machines and assign it to USB drive on the first use but that's no fun. I decided to do it the PowerShell way. I wrote a small function to find a System.IO.DriveInfo object by providing the volume label and then I can get the drive letter and pass it to Copy-Item to do the work.

function Get-DriveInfo ([string] $label = $(throw "Please specify a volume label.")) { 
   [System.IO.DriveInfo]::GetDrives() | ? { $_.IsReady -eq $True -and $_.VolumeLabel -match $label }
}

 Wednesday, June 20, 2007

PowerCleaning

I've often wanted to easily delete all old files below a folder that haven't been modified for a while. I've written a very small PowerShell function to do just that. By default it emulates the Disk Cleanup tool in Windows and deletes all files older than one week from the environment temp folder.

function Remove-TemporaryItems ([string] $path = ($env:TEMP), [TimeSpan] $age = (New-Object System.TimeSpan 7, 0, 0, 0) ) {
    $files = Get-ChildItem $env:TEMP -recurse | `
        Where-Object { ! $_.PSIsContainer -and $_.LastWriteTime.Add($age) -le [DateTime]::Now };
    $files | Remove-Item;
    $files;
}