PowerShell 2.0 CTP now Out!

Filed under: PowerShell — Written by Chrissy on Sunday, November 11th, 2007 @ 5:44 am

Not even one-handed typing with a gimp shoulder in a rotator cuff cast can stop me from blogging this... PowerShell 2.0 CTP is now available!

I won't get to play for weeks, sadly. So...on an unrelated note; anyone know what's the best dictation software out there? Thanks.

PowerShell: Parse an RSS Feed and E-mail/Text Yourself the Results

Filed under: PowerShell, Quick Code — Written by Chrissy on Thursday, October 25th, 2007 @ 1:30 pm

I'm so excited -- my best friend and tech partner-in-crime, Brandon, recently picked up PowerShell after I gave him a copy of Wrox's Professional PowerShell. He totally loves it and has already created a few new PowerShell scripts and converted other ASP.NET/VBScript scripts. One of his favorite scripts checks the Woot.com RSS feed and e-mails him with the daily item. He reduced a 25 line VBScript to a 5 line PowerShell script which goes something like this:

$rssUrl = "http://www.woot.com/blog/rss.aspx"
$blog = [xml](new-object System.Net.WebClient).DownloadString($rssUrl)
$results = $blog.rss.channel.item[0].title
 
$smtpmail = [System.Net.Mail.SMTPClient]("smtp.san.rr.com")
$smtpmail.Send("me@netnerds.net", "xxxxxxxxxx@vtext.com ", "Woot Deal", $results)

Five silly lines! PowerShell is just so straightforward:

1) Assign URL to a variable
2) Create a new WebClient and pull the RSS Feed into it.
3) Assign the value of the first title element to a variable
4) Create a SMTP Client and assign it the value of the smtp host
5) Access the Send Method and pass it (From, To, Subject, Body)

He then used Task Scheduler (tasksch) to run the script each night.

Another script he wrote checks for backups older than seven days then deletes them. If you use this script, be sure to remove the -whatif if you are happy with the results.

foreach ($file in (get-childitem -Path C:\backups\))
{
  $curTime = [System.DateTime]::get_now()
  if ($file.LastWriteTime -gt $curTime.Add("-7"))
   {
    remove-item -recurse $file -whatif
   }
}

For the record, I have Professional Windows PowerShell (Wrox/Andrew Watt), Windows PowerShell Cookbook (O'reilly/Lee Holmes, MSFT) and PowerShell in Action (Manning/Bruce Payette, MSFT). I suggest reading all three of them, in that order. The Wrox and O'reilly books provide the "how" while Payette's book provides the "why?". All three are very well written, especially the Cookbook, for which I was a technical editor ;)

PowerShell 1.0: Adding Virtual FTP Directories to IIS 6 or 7

Filed under: IIS, PowerShell, Quick Code — Written by Chrissy on Sunday, July 29th, 2007 @ 4:41 pm

While my firm explores using WebDAV and SharePoint 2007 for exchanging large amounts of files, we're temporarily using FTP dropboxes to fill the void. Last Monday, I setup 11 new accounts and it took a total of one hour to complete the same 15 step process (give or take) for each account. By the time I was finished, I decided automating FTP account creation would be my first PowerShell project. What you see below is part of that project.

The code below creates a virtual directory in the "Default FTP Site" of the machine that is running the PowerShell script. The virtual directory called "NewUser" is mapped to C:\FTP\NewUser and is set to be both readable and writable. For the record, I couldn't get WMI to work (get-wmiobject) and that's the reason I decided to use the .NET's Directory Services support.

$server = $env:computername
$service = New-Object System.DirectoryServices.DirectoryEntry("IIS://$server/MSFTPSVC")
$site = $service.psbase.children |Where-Object { $_.ServerComment -eq 'Default FTP Site' }
$site = New-Object System.DirectoryServices.DirectoryEntry($site.psbase.path+"/Root") # <-- IIS 6 requires this. Not sure why. Otherwise, it never appears to commit changes. This line is not required for IIS 7.
$virtualdir = $site.psbase.children.Add("NewUser","IIsFtpVirtualDir")
$virtualdir.psbase.CommitChanges()
$virtualdir.put("Path","C:\FTP\NewUser")
$virtualdir.put("AccessRead",$true)
$virtualdir.put("AccessWrite",$false)
$virtualdir.psbase.CommitChanges()
$service.psbase.refreshCache() # OPTIONAL

Alternatively, you could do go straight for the path if you know it (IIS 6 seems to like this):

$service = New-Object DirectoryServices.DirectoryEntry("IIS://localhost/MSFTPSVC/1/Root")
$virtualdir = $service.psbase.children.Add("NewUser", "IIsFtpVirtualDir")
$virtualdir.psbase.CommitChanges()
$virtualdir.put("Path","C:\FTP\NewUser")
$virtualdir.put("AccessRead",$true)
$virtualdir.put("AccessWrite",$false)
$virtualdir.psbase.CommitChanges()

If you would like to iterate through each of the virtual directories on your FTP server, you can use the following code:

$service = New-Object System.DirectoryServices.DirectoryEntry("IIS://$env:computername/MSFTPSVC")
$site = $service.psbase.children | Where-Object { $_.ServerComment -eq 'Default FTP Site' }
$virtualdirs = $site.psbase.children.Find("Root","IIsFtpVirtualDir").psbase.children
foreach ($virtualdir in $virtualdirs) {$virtualdir.psbase.name}

This code is likely applicable to many of the objects in the IIS ADSI provider. While I've only tested this on Vista (IIS 7), this should also work for Windows XP and 2003's IIS 6 as Vista uses IIS 6's MMC for management.

Also, if you are wondering how I know when to use psbase or psbase.children, I really don't. I just fumble around until I get it to work. The 4 lines above, specifically $virtualdirs = $site.psbase.children.Find("Root","IIsFtpVirtualDir").psbase.children took me about seven hours to figure out. I hear PowerShell 2.0 will have much better support for Directory Services and hopefully that will include support the IIS FTP service.

PowerShell: Set-Acl Does Not Appear to Work

Filed under: PowerShell, Quick Code, Security — Written by Chrissy on Saturday, July 28th, 2007 @ 7:32 am

If you've ever dealt with NTFS permissions in VBScript, you will no doubt appreciate just how easy PowerShell now makes it to manage access control lists. Basic examples in PowerShell books and around the 'net look something like this:

$directory = "Test"
$acl = Get-Acl $directory
$accessrule = New-Object system.security.AccessControl.FileSystemAccessRule("IUSR_CRACKLIN", "Modify", "Allow")
$acl.AddAccessRule($accessrule)
set-acl -aclobject $acl $directory

In the example above, user "IUSR_CRACKLIN" is given Modify access to the Test directory. Running the code above will not produce any errors but upon checking permission via the GUI, it seems as though the user was added, but no permissions were set.

I thought that perhaps this was an issue with Vista and I tried it on Windows Server 2003. And that's when I noticed that the directory had been given "Special Permissions." When I checked the Advanced permissions, I could see that Modify access had been assigned, but only to "This Folder." Other folders that had the checkboxes checked listed "This Folder, subfolders and files"

Since I wanted the Test directory permissions to match the others, I searched the Google to see which flags would give me "This Folder, subfolders and files." I found Damir Dobric's blog post titled "Directory Security and Access Rules which sported a handy reference table flags that must be set to achieve various scenarios.

Subfolders and Files only InheritanceFlags.ContainerInherit, InheritanceFlags.ObjectInherit, PropagationFlags.InheritOnly
This Folder, Subfolders and Files    InheritanceFlags.ContainerInherit, InheritanceFlags.ObjectInherit, PropagationFlags.None
This Folder, Subfolders and Files InheritanceFlags.ContainerInherit, InheritanceFlags.ObjectInherit, PropagationFlags.NoPropagateInherit
This folder and subfolders InheritanceFlags.ContainerInherit, PropagationFlags.None
Subfolders only InheritanceFlags.ContainerInherit, PropagationFlags.InheritOnly
This folder and files InheritanceFlags.ObjectInherit, PropagationFlags.None
This folder and files InheritanceFlags.ObjectInherit, PropagationFlags.NoPropagateInherit

So it setting the following should give me what I need:
InheritanceFlags.ContainerInherit, InheritanceFlags.ObjectInherit and PropagationFlags.None
.

$directory = "Test"
$inherit = [system.security.accesscontrol.InheritanceFlags]"ContainerInherit, ObjectInherit"
$propagation = [system.security.accesscontrol.PropagationFlags]"None"
$acl = Get-Acl $directory
$accessrule = New-Object system.security.AccessControl.FileSystemAccessRule("IUSR_CRACKLIN", "Modify", $inherit, $propagation, "Allow")
$acl.AddAccessRule($accessrule)
set-acl -aclobject $acl $directory

I then checked the permissions and voila:

Imagine that.. PowerShell can set any number of permissions with about 6 lines of code while VBScript requires over 36 lines JUST to set the constants needed for managing permissions. I'm so excited thinking about the possibilities: PowerShell + Windows Core + SSH is going to be awesome.

Powershell: Working with Passwords

Filed under: Active Directory, PowerShell, Security — Written by Chrissy on Thursday, July 26th, 2007 @ 9:30 pm

When creating a new Active Directory user from the command line in PowerShell, you will likely find yourself using Read-Hosts's asSecureString switch when entering the password.


$password = Read-Host "Enter password" -AsSecureString

Next, you'll probably look around the Internets for a few hours or so trying to figure out how to change the password of the newly created user. You will soon discover that the user creation process in PowerShell 1.0 isn't very straightfoward and it even requires a specific order for proper account creation. First, you create the account, then you set some basic properties, next you call SetInfo(), and finally you invoke setPassword using the follwing syntax:


$newUser.psbase.Invoke("SetPassword",$password)

Now you may find yourself with the following exception: Exception calling "Invoke" with "2" argument(s): "Exception has been thrown by the target of an invocation." Originally, this post mentioned using toString() to address the problem but PowerShell team member Lee Holmes wrote to let me know that the password was changed literally to System.Security.SecureString. He also said that "there is no really easy way to convert a secure string to plain text - on purpose. Since a SecureString is supposed to prevent plain text from littering your computer's memory, converting it to plain text defeats the purpose."

My primary reason for using asSecureString is to encode the string into asterisks when typing it at the prompt. So Lee gave me two ways to convert the password to be used while invoking SetPassword. Note that unless you are using a secure LDAP channel, the password will be sent over the network in clear text.


$temporaryCredential = New-Object System.Management.Automation.PsCredential "None",$password
$newUser.psbase.Invoke("SetPassword",$temporaryCredential.GetNetworkCredential().Password)

Or, alternatively:


$temporaryCredential = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($password))$newUser.psbase.Invoke("SetPassword",$temporaryCredential)

If you continue to see this exception, check to make sure that the password you entered meets your domain's password complexity requirements.

PowerShell 1.0: Find the Fully Qualified Domain Name of Current Active Directory Domain

Filed under: Active Directory, PowerShell, Quick Code — Written by Chrissy on Thursday, July 26th, 2007 @ 9:52 am

So I'm making the move to PowerShell. It's painful learning such alien (to me) concepts but books like Lee Holmes' PowerShell: The Definitive Guide help a ton. I was fortunate enough to be the editor for Chapters 1-5 and got a sneak preview. It's a fantastic book and can't wait to receive the title, complete with indexes! For now, I'm searching both the 36 Word documents and the sample code for solutions using Vista's built-in search functions.

My first task, which I'll explain in later posts, includes some AD stuff. One tiny part of the equation is dynamically finding the FQDN of the current Active Directory domain. This should be easy! Using RootDSE, it's super easy to find out the DistinguishedName or even the FQDN of the domain controller being queried, so wouldn't there be a similar entry for FQDN of the whole domain? Apparently not (or if it's there, I can't find it). I've spent the morning and part of last night digging through LDAP filters, looking on Google for examples of objectcategory=crossref, dnsroot, dnshostname, etc. But after finding this useful codeplex page, I played around with GetCurrentDomain() and realized that finding the DNS hostname for an AD domain all boils down to this one line:

$strDomainDNS = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain().Name

Or this one liner, as Lee just suggested to me

$strDomainDNS = $env:USERDNSDOMAIN

Ahh! So it was that easy. Why, then, did it take 8 hours to find that? Now I wonder how to get the FQDN of any domain. Being a newb is hard work.

If you're wondering what dnshostname is for, it's to resolve the local machine you are working with. Michael at brnets.com provides the following example embedded in a big ol Exchange script:

$rootDSE = [adsi]"LDAP://RootDSE"
$DCDNShostname = $rootDSE.dnsHostName
$DCDNShostname

Oh, as a bonus, here's some another query you may find helpful. I started with benp's script then made it a little skinnier (and more prone to errors! ;))

Search for Active Directory User Object in the Current Domain

$domain = New-Object DirectoryServices.DirectoryEntry
$search = [System.DirectoryServices.DirectorySearcher]$domain
$search.Filter = "(&(objectClass=user)(sAMAccountname=Chrissy))"
$user = $search.FindOne().GetDirectoryEntry()
$user.Name

My original code made reference to GetDirectoryEntry() but John Brennan suggested looking for alternative approaches. Thanks for the tip, John!