Friday, January 28, 2011

Validating Command Line Parameters

A common reason I write scripts is to provide other members of my IT community the ability to perform functions that standard tools cannot satisfy. Many of these scripts require multiple parameters in order to return the desired result. The last thing I want to do is take to time to explain why something I wrote failed to provide the desired result because of an input error. Human error when someone else is using your work should be forefront in your mind. Evaluating the parameters supplied to a script and making sure that they meet a certain level of sanity reduces those chances.

You can do basic checking directly in the param using Read-Host but that just insures a value is provided.
param([string]$path = $(Read-Host -prompt "`tPath"))
This will not guarantee that the data the script is expecting will be provided by the user nor can it provide confirmation that it is what the user meant. This can be very crucial when dealing with scripts that may delete or modify objects. Sometimes, you may even want to go the extra mile and ask, "Are you really sure?". The following code is a typical interrogation I would do when asking for a path to be supplied.

if(![bool]$path) {
 Write-Host "You are missing the `"-path`" command line argument" -foregroundColor Red
 Write-Host ""
 Write-Host "This is the path that the script requires to perform its function."
 Write-Host ""
 while((![bool]$path)) {
  $path = Read-Host -prompt "`tPath"
 Write-Host ""
if(!(Test-Path -path $path)) {
 Write-Host "Unable to locate: $path" -foregroundColor Red
 Write-Host ""
 Write-Host "Please review the correct path"
 Write-Host ""
 while(!(Test-Path -path $path)) {
  $path = Read-Host -prompt "`tPath"
 Write-Host ""

$answer = $null
while($answer -ne "Y" -and $answer -ne "N") {
 $answer = Read-Host -prompt "Are you sure you want to use $path (Y/N)?"

if($answer -eq "N") {
 Write-Host ""
 Write-Host "Quiting..." -foregroundColor Yellow

Write-Host ""
Write-Host "`$path = $path"

Thursday, January 27, 2011

List of Attributes in the Partial Attribute Set

In many of my previous posts, I have mentioned the set of attributes stored in the Global Catalog called the "Partial Attribute Set". While there is a standard list, an administrator of a forest can promote other attributes into the partial attribute set or some products will add attributes through a schema extension. The code listed below will display what is currently queryable via the Global Catalog about objects stored within it. Ironically, the attribute isMemberOfPartialAttributeSet is not a member of the partial attribute set. It uses Write-Output to display the resulting data so you can output it to a text file for review.
$forestInformation = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()
$rootDse = [ADSI]("LDAP://" + ($forestInformation.Name) + "/rootDSE")

$objectConnection = New-Object -comObject "ADODB.Connection"
$objectCommand = New-Object -comObject "ADODB.Command"
$objectCommand.ActiveConnection = $objectConnection

$ldapBase = ("LDAP://" + ($forestInformation.SchemaRoleOwner.Name) + "/" + ($rootDse.schemaNamingContext).ToString())
$ldapAttr = "lDAPDisplayName"
$ldapScope = "subtree"
$ldapFilter = "(&(objectClass=attributeSchema)(isMemberOfPartialAttributeSet=TRUE))"
$ldapQuery= "<$ldapBase>;$ldapFilter;$ldapAttr;$ldapScope"
$objectCommand.CommandText = $ldapQuery
$objectRecordSet = $objectCommand.Execute()

$title = "Attributes in the Global Catalog"
Write-Output $title
Write-Output ("-" * $title.Length)

while(!$objectRecordSet.EOF) {
 Write-Output $objectRecordSet.Fields.Item('lDAPDisplayName').Value

Wednesday, January 26, 2011

Export OCS Conferencing Users with a Registered Conference ID & PIN

If you are exploiting the full capabilities of Office Communicator Server 2007 (now called Lync 2010), you have it connected to a telephony gateway and have enabled users to register for audio conferencing. To determine who has signed up and registered a PIN, the following PowerShell script will return a comma separated value file with some basic information. In this example, it uses System.Data.SQLClient to assist in the SQL query against the rtc database to pull the e-mail address associated to the registered row. Using that e-mail address, I craft an LDAP query using Microsoft ActiveX Data Object (there are other methods) and targeting a Global Catalog (GC://) to perform the query. The purpose of using the Global Catalog to perform the query relates to a multi-domain forest environment and the proxyAddresses attribute is a member of the partial attribute set contained within the Global Catalog. All the attributes provided in this script also reside in the partial attribute set. If you were to change up the returned attributes, please be aware that you need to consult the partial attribute set to ensure you are able to retrieve the attributes you want. If they are not, you can either chase referrals or use the returned distinguisedName to perform an LDAP:// query against a specific domain.
$ocsConferencingUsers = @()
$ocsConferencingEntries = @()

$sqlConnection = New-Object System.Data.SQLClient.SQLConnection
$sqlConnection.ConnectionString = ";database=rtc;trusted_connection=true;"

$sqlCommand = New-Object System.Data.SQLClient.SQLCommand
$sqlCommand.Connection = $sqlConnection
$sqlCommand.CommandText = "SELECT UserAtHost FROM dbo.Resource, dbo.UserPinMembership WHERE dbo.Resource.ResourceId = dbo.UserPinMembership.ResourceId"

$sqlDataReader = $sqlCommand.ExecuteReader()

if($sqlDataReader.HasRows -eq  $true) {
 while($sqlDataReader.Read()) {
  $ocsConferencingEntries += $sqlDataReader.GetValue(0)
} else {
 Write-Warning "Unable to obtain OCS data from SQL"

$objectConnection = New-Object -comObject "ADODB.Connection"
$objectCommand = New-Object -comObject "ADODB.Command"
$objectCommand.ActiveConnection = $objectConnection

$forestRootDn = ([ADSI]("LDAP://" + (([System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()).name) + "/rootDSE")).defaultNamingContext

$ldapBase = "GC://$forestRootDn"
$ldapAttr = "distinguishedName,sAMAccountName,givenName,sn,mail,telephoneNumber,streetAddress,l,st,postalCode,c"
$ldapScope = "subtree"

foreach($ocsConferencingEntry in $ocsConferencingEntries) {

 $ldapFilter = "(&(objectClass=user)(proxyAddresses=smtp:$ocsConferencingEntry))"
 $ldapQuery = "<$ldapBase>;$ldapFilter;$ldapAttr;$ldapScope"
 $objectCommand.CommandText = $ldapQuery
 $objectRecordSet = $objectCommand.Execute()

 while(!$objectRecordSet.EOF) {
  $userObjectDn = $objectRecordSet.Fields.Item('distinguishedName').Value
  $userObjectDomain = ((($userObjectDn -replace "(.*?)DC=(.*)",'$2') -replace "DC=","") -replace ",",".")
  $ocsConferencingUser = New-Object -typeName PSObject
  Add-Member -inputObject $ocsConferencingUser -type NoteProperty -name "domain" -value $userObjectDomain.Split(".")[0]
  Add-Member -inputObject $ocsConferencingUser -type NoteProperty -name "sAMAccountName" -value $objectRecordSet.Fields.Item('sAMAccountName').Value
  Add-Member -inputObject $ocsConferencingUser -type NoteProperty -name "firstName" -value $objectRecordSet.Fields.Item('givenName').Value
  Add-Member -inputObject $ocsConferencingUser -type NoteProperty -name "lastName" -value $objectRecordSet.Fields.Item('sn').Value
  Add-Member -inputObject $ocsConferencingUser -type NoteProperty -name "eMail" -value $objectRecordSet.Fields.Item('mail').Value
  Add-Member -inputObject $ocsConferencingUser -type NoteProperty -name "telephoneNumber" -value $objectRecordSet.Fields.Item('telephoneNumber').Value
  Add-Member -inputObject $ocsConferencingUser -type NoteProperty -name "streetAddress" -value ($objectRecordSet.Fields.Item('streetAddress').Value -replace "`r`n",", ")
  Add-Member -inputObject $ocsConferencingUser -type NoteProperty -name "city" -value $objectRecordSet.Fields.Item('l').Value
  Add-Member -inputObject $ocsConferencingUser -type NoteProperty -name "state" -value $objectRecordSet.Fields.Item('st').Value
  Add-Member -inputObject $ocsConferencingUser -type NoteProperty -name "zipCode" -value $objectRecordSet.Fields.Item('postalCode').Value
  Add-Member -inputObject $ocsConferencingUser -type NoteProperty -name "country" -value $objectRecordSet.Fields.Item('c').Value
  $ocsConferencingUsers += $ocsConferencingUser


$ocsConferencingUsers | Export-Csv -path "ocsConferencingUsers.csv" -noTypeInformation

Friday, January 21, 2011

Return a Local Domain Controller in the Current Site for a Specific Domain

Below is a good one liner to return a local Domain Controller for a specific domain in the current site of the computer running a PowerShell script. In the example script, we are taking a known distinguished name, obtaining the domain it resides using code from this post, using that domain information to obtain a local domain controller in the current site using the one liner and finally obtaining the user object from Active Directory so we can output attributes -- in this case, an e-mail address. This script assumes that you have one valid domain controller for the specific domain in the current site. In a multi-domain forest, the security context you execute a PowerShell script will not always be in the same domain as the object you are interested. This can cause LDAP queries to fail (unless you are using chase referrals which are slow) and it is not always suitable to use a Global Catalog query as the attributes you are interested in may not be in the Partial Attribute Set.
$distinguishedName = "CN=Doe\, John,OU=Advertising,OU=User,OU=New York,OU=Accounts,DC=usa,DC=corp,DC=foobar,DC=local"
$objectDomain = ((($distinguishedName -replace "(.*?)DC=(.*)",'$2') -replace "DC=","") -replace ",",".")
# Get a local domain controller
$localDomainController = ([System.DirectoryServices.ActiveDirectory.ActiveDirectorySite]::GetComputerSite()).Servers | Where-Object { $_.Domain.Name -eq $objectDomain } | ForEach-Object { $_.Name } | Select-Object -first 1
$userObject = [ADSI]"LDAP://$localDomainController/$distinguishedName"
Write-Host $userObject.mail

Thursday, January 20, 2011

Force Mac OS X to Re-Register with Dynamic DNS

In our heterogenous, corporate computing community, Active Directory no longer only supports Windows based clients. In many companies, Mac OS X workstations & servers are bound to the directory. One issue I have encountered from time to time are Mac OS X bound clients not registering themselves in Active Directory integrated DNS. With a Windows client, you would either run ipconfig /registerdns or restart the DHCP Client to force a re-registration in DNS. Neither of those options are available for Mac OS X client. In Mac OS X, start a terminal session type sudo net ads dns register.  If you want more information about the binding of your Mac OS X system, run net ads status from your Terminal session.

Wednesday, January 19, 2011

File System Rights -- Part 1

A common request I receive is "Who has rights to this?". To know this, we need to know the access rights and access masks for the securable object. The access mask is a 32 bit value that we can determine the access rights. To obtain this information in PowerShell, we use the Get-Acl cmdlet to obtain the security of the file system object. In the code below, we use Get-Acl and query the access rules and perform bitwise and operations to obtain the masks for each identity. This is a good first start in understanding how to return the information for "Who has rights to this?" but that return might not be easily digestible to the typical user. In a future, I will supply a function that will return exactly what appears in the advanced permissions tab on a file or folder -- a little more english than techish.
Function Get-Permissions($fileSystemRights) {

 $permissions = @()

# Read access
 if($fileSystemRights -band 0x80000000) { $permissions += "GENERIC_READ" }
# Write access
 if($fileSystemRights -band 0x40000000) { $permissions += "GENERIC_WRITE" }
# Execute access
 if($fileSystemRights -band 0x20000000) { $permissions += "GENERIC_EXECUTE" }
# Read, write, and execute access
 if($fileSystemRights -band 0x10000000) { $permissions += "GENERIC_ALL" }

 if($fileSystemRights -band 0x8000000) { $permissions += "RESERVED_1" }
 if($fileSystemRights -band 0x4000000) { $permissions += "RESERVED_2" }
 if($fileSystemRights -band 0x2000000) { $permissions += "MAXIMUM_ALLOWED" }
 if($fileSystemRights -band 0x1000000) { $permissions += "ACCESS_SYSTEM_SECURITY" }
 if($fileSystemRights -band 0x800000) { $permissions += "UNKNOWN" }
 if($fileSystemRights -band 0x400000) { $permissions += "UNKNOWN" }
 if($fileSystemRights -band 0x200000) { $permissions += "UNKNOWN" }
# Essentially Full Control
 if($fileSystemRights -band 0x1F01FF) { $permissions += "FILE_ALL_ACCESS" }
 if($fileSystemRights -band 0x1F0000) { $permissions += "STANDARD_RIGHTS_ALL" }

 if($fileSystemRights -band 0xF0000) { $permissions += "STANDARD_RIGHTS_REQUIRED" }

# Read
 if($fileSystemRights -band 0x120089) { $permissions += "FILE_GENERIC_READ" }
 if($fileSystemRights -band 0x12010E) { $permissions += "FILE_GENERIC_WRITE" }
 if($fileSystemRights -band 0x1200A0) { $permissions += "FILE_GENERIC_EXECUTE" }
# Currently defined to equal READ_CONTROL.
 if($fileSystemRights -band 0x20000) { $permissions += "STANDARD_RIGHTS_READ" }
# Currently defined to equal READ_CONTROL.
 if($fileSystemRights -band 0x20000) { $permissions += "STANDARD_RIGHTS_WRITE" }
# Currently defined to equal READ_CONTROL.
 if($fileSystemRights -band 0x20000) { $permissions += "STANDARD_RIGHTS_EXECUTE" }
 if($fileSystemRights -band 0xFFFF) { $permissions += "SPECIFIC_RIGHTS_ALL" }

#  permission to synchronize
#  bit indicating permission to perform synchronize operation, used sometimes during file access
#  this permission is automaticaly granted with read and write access and revoked when read or write access is denied. 
#  it is not displayed in the list of permissions in Windows UI
 if($fileSystemRights -band 0x100000) { $permissions += "SYNCHRONIZE" }
#  permission to assign owner
#  corresponds to Take Ownership permissions in Windows UI
 if($fileSystemRights -band 0x80000) { $permissions += "WRITE_OWNER" }
#  permission to change discretionary ACL
#  corresponds to Change Permissions permissions in Windows UI
 if($fileSystemRights -band 0x40000) { $permissions += "WRITE_DAC" }
#  permission to read security descriptor
#  corresponds to Read Permissions permissions in Windows UI
 if($fileSystemRights -band 0x20000) { $permissions += "READ_CONTROL" }
#  permission to delete file or folder
#  corresponds to Delete permissions in Windows UI
 if($fileSystemRights -band 0x10000) { $permissions += "DELETE" }
 if($fileSystemRights -band 0x10000) { $permissions += "FILE_DELETE" }
#  permission to change file or folder attributes
#  corresponds to Write Attributes permissions in Windows UI
 if($fileSystemRights -band 0x100) { $permissions += "FILE_WRITE_ATTRIBUTES" }
#  permission to read file or folder attributes
#  corresponds to Read Attributes permissions in Windows UI
 if($fileSystemRights -band 0x80) { $permissions += "FILE_READ_ATTRIBUTES" }
#  permission to delete directory and all files it contains
#  corresponds to Delete Subfolders and Files permissions in Windows UI
 if($fileSystemRights -band 0x40) { $permissions += "FILE_DELETE_CHILD" }

#  permission to execute file or traverse directory
#  corresponds to Traverse Folder / Execute File permissions in Windows UI
 if($fileSystemRights -band 0x20) { $permissions += "FILE_EXECUTE" }
 if($fileSystemRights -band 0x20) { $permissions += "FILE_TRAVERSE" }
#  permission to write extended attributes
#  corresponds to Write Extended Attributes permissions in Windows UI
 if($fileSystemRights -band 0x10) { $permissions += "FILE_WRITE_EA" }
#  permission to read extended attributes
#  corresponds to Read Extended Attributes permissions in Windows UI
 if($fileSystemRights -band 0x8) { $permissions += "FILE_READ_EA" }

#  permission to append data to file or to create subdirectory
#  corresponds to Create Folders / Append Data permissions in Windows UI
 if($fileSystemRights -band 0x4) { $permissions += "FILE_APPEND_DATA" }
 if($fileSystemRights -band 0x4) { $permissions += "FILE_ADD_SUBDIRECTORY" }
#  permission to write data to file or create file in directory
#  corresponds to Create Files / Write Data permissions in Windows UI
 if($fileSystemRights -band 0x2) { $permissions += "FILE_WRITE_DATA" }
 if($fileSystemRights -band 0x2) { $permissions += "FILE_ADD_FILE" }
#  permission to read data from file or list contents of directory
#  corresponds to List Folder / Read Data permissions in Windows UI
 if($fileSystemRights -band 0x1) { $permissions += "FILE_READ_DATA" }
 if($fileSystemRights -band 0x1) { $permissions += "FILE_LIST_DIRECTORY" }

 return $permissions

$uncPath = "\\\share\directory"
#$uncPath = "\\\share\directory\file.txt"
if(Test-Path -path $uncPath) {
 Write-Host $uncPath -foregroundColor Yellow
 $acl = Get-Acl -path $uncPath 
 $aces = $acl.GetAccessRules($true, $true, [System.Security.Principal.NTAccount])
 foreach($ace in $aces) {
  $identityReference = $ace.IdentityReference.Value
  Write-Host $identityReference -foregroundColor Green
  $permissions = Get-Permissions $ace.FileSystemRights
  foreach($permission in $permissions) {
   Write-Host "`t$permission"
  Write-Host ("-" * 70)

Tuesday, January 18, 2011

Resolving System State Backup failures in TSM

An issue with IBM Tivoli backups I encounter on regular basis are failures with the System State portion of the TSM backup process (ANS5258E error in the dsmsched.log). IBM would have you reboot the server to resolve the issue. This solution works but we are better than that. The issue lies with Volume Shadow Service writers. To determine the culprit, I need to know which of the writers is having the issue. I use the command line program "vssadmin" to discover the problematic writer.

Sample Output:

C:\>vssadmin list writers
vssadmin 1.1 - Volume Shadow Copy Service administrative command-line tool
(C) Copyright 2001 Microsoft Corp.

Writer name: 'System Writer'
   Writer Id: {e8132975-6f93-4464-a53e-1050253ae220}
   Writer Instance Id: {ea8c901f-77c6-4a3e-a673-7d2276d7a4bf}
   State: [7] Failed
   Last error: No error

Writer name: 'Event Log Writer'
   Writer Id: {eee8c692-67ed-4250-8d86-390603070d00}
   Writer Instance Id: {d787de6c-2b5d-45b9-8916-4351737ec993}
   State: [1] Stable
   Last error: No error

Writer name: 'Registry Writer'
   Writer Id: {afbab4a2-367d-4d15-a586-71dbb18f8485}
   Writer Instance Id: {76401cb9-4cf5-4c0f-bf8f-2adb4672f119}
   State: [1] Stable
   Last error: No error

Writer name: 'COM+ REGDB Writer'
   Writer Id: {542da469-d3e1-473c-9f4f-7847f01fc64f}
   Writer Instance Id: {59267df6-52ac-4679-ac8e-536527bb195f}
   State: [1] Stable
   Last error: No error

Writer name: 'WMI Writer'
   Writer Id: {a6ad56c2-b509-4e6c-bb19-49d8f43532f0}
   Writer Instance Id: {21441d8f-4ce4-4c53-b817-f7f899a154c0}
   State: [1] Stable
   Last error: No error

Writer name: 'IIS Metabase Writer'
   Writer Id: {59b1f0cf-90ef-465f-9609-6ca8b2938366}
   Writer Instance Id: {a5e5576d-9f8d-4933-9f51-c0e0895eafaf}
   State: [1] Stable
   Last error: No error

Writer name: 'BITS Writer'
   Writer Id: {4969d978-be47-48b0-b100-f328f07ac1e0}
   Writer Instance Id: {078d566b-625c-4daa-9535-9546a57ea482}
   State: [1] Stable
   Last error: No error

If any of these writers report anything but their state as 'Stable' and last error as 'No error',  you need to restart the associated service for that writer. In the above example, we would restart Cryptographic Services to resolve the System Writer issue. Here is a list of writers and associated services I have collected and is by no means a comprehensive list but what I have encountered the most when resolving this issue.
  • System Writer -- restart Cryptographic Services
  • Event Log Writer -- restart Event Log (never seen this before)
  • Registry Writer -- restart Volume Shadow Copy
  • COM+ REGDB Writer -- restart Volume Shadow Copy
  • WMI Writer -- restart Windows Management Instrumentation
  • IIS Metabase Writer -- restart IIS Admin Service (not always 100% effective)
  • BITS Writer -- restart Background Intelligent Transfer Service
  • Shadow Copy Optimization Writer -- restart Volume Shadow Copy
The simple way to check if you have resolved your issue is to rerun 'vssadmin list writers' and ensure that no writers have issues then run 'dsmc ba -optfile=dsm.opt' from the baclient directory and watch the manual backup for system state success. 

Friday, January 14, 2011

Replicate UNIX 'cal' in PowerShell

This is a good exercise in using dates in a loop. This code replicates somewhat the functionality of the command line application 'cal' that first appeared in Version 5 AT&T UNIX. In the code, you will perform while loops based on months and days. Save the code in a file called 'Output-Calendar.ps1'. If you provide no parameters, it will display a calendar of the current month. If you provide a valid date as a parameter, it will display that month's calendar. If you provide two months, it will generate all the calendars between those two dates. It does not validate if the input are valid dates and assumes the culture is English.


.\Output-Calendar.ps1 1/2000
.\Output-Calendar.ps1 1/1/2010 12/31/2010

Function Get-LastDayOfMonth($date) {
 return ((Get-Date ((((Get-Date $date).AddMonths(1)).Month).ToString() + "/1/" + (((Get-Date $date).AddMonths(1)).Year).ToString()))) - (New-TimeSpan -seconds 1)

Set-Variable -name dayOfWeekLine -option Constant -value " Su Mo Tu We Th Fr Sa "
if($start -eq "" -and $end -eq "") {
 $startDate = Get-Date
 $endDate = Get-Date
} elseif($end -eq "") {
 $startDate = Get-Date $start
 $endDate = Get-Date $start
} else {
 $startDate = Get-Date $start
 $endDate = Get-Date $end 
if($startDate -gt $endDate) {
 Write-Warning "The Start Date must be earlier than the End Date"
$month = $startDate
Write-Host ""
while($month -le $endDate) {
 $firstDayOfMonth = Get-Date ((((Get-Date $month).Month).ToString() + "/1/" + ((Get-Date $month).Year).ToString() + " 00:00:00"))
 $lastDayOfMonth = Get-LastDayOfMonth $firstDayOfMonth
 $day = $firstDayOfMonth
 $headline = ((Get-Date $firstDayOfMonth -Format MMMM) + " " + $firstDayOfMonth.Year)
 Write-Host (" " * (($dayOfWeekLine.Length - $headline.Length) / 2)) -noNewline
 Write-Host $headline
 Write-Host $dayOfWeekLine
 while($day -le $lastDayOfMonth) {
  if($ -eq 1 -and $day.DayOfWeek -ne "Sunday") {
   Write-Host " " -noNewline
   if($day.DayOfWeek -eq "Saturday") {
    Write-Host (" " * 15) -noNewline
   } elseif($day.DayOfWeek -eq "Friday") {
    Write-Host (" " * 12) -noNewline
   } elseif($day.DayOfWeek -eq "Thursday") {
    Write-Host (" " * 9) -noNewline
   } elseif($day.DayOfWeek -eq "Wednesday") {
    Write-Host (" " * 6) -noNewline
   } elseif($day.DayOfWeek -eq "Tuesday") {
    Write-Host (" " * 3) -noNewline
  if($day.DayOfWeek -eq "Saturday") {
   Write-Host (" " + (Get-Date $day -Format dd))
  } else {
   Write-Host (" " + (Get-Date $day -Format dd)) -noNewline
  $day = $day.AddDays(1)
 if($day.DayOfWeek -ne "Sunday") {
  Write-Host ""
 Write-Host ""
 $month = $firstDayOfMonth.AddMonths(1)

Thursday, January 13, 2011

Quick Active Directory Sites & Subnets Report

This is a quick report I wrote up to generate a list of sites & subnets within a Forest to compare against network topology. Pipe it out to a text file and mail it off to your networking team (which might be you!).
$forestInformation = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()
foreach($site in $forestInformation.Sites | Sort-Object -property Name) {
   Write-Output $site.Name
   Write-Output ("-" * ($site.Name).Length)
   foreach($subnet in $site.Subnets | Sort-Object) {
      Write-Output "`t$subnet"
   Write-Output ""

Wednesday, January 12, 2011

Searching for Files in Nested Directories

Sometimes I need more fine grained control over searching for files in nested directories than the "-recurse" option for Get-ChildItem provides or I don't want to write some extremely long piped command to achieve the desired result. Below is a code snippet that is a good jumping off point to achieving that goal. This snippet will search the C: drive for Office documents and text files and output them to a comma separated value file. You could swap the local path to a UNC path and search a network share.
Function Search-NestedDirectory($path,$files) {
 foreach($childItem in (Get-ChildItem -literalPath $path -force | Sort-Object -property FullName)) {
  if($childItem.PSIsContainer -eq "True") {
   Write-Host ("Scanning: " + $childItem.FullName) -foregroundColor Green
   if($files.count -ge 1) {
    Write-Host ("Files Found: " + $files.count)
   $files = Search-NestedDirectory $childItem.FullName @($files)
  } elseif($childItem.Extension -eq ".xls" -or $childItem.Extension -eq ".doc" -or $childItem.Extension -eq ".ppt" -or $childItem.Extension -eq ".txt") {
   $file = New-Object -typeName PSObject
   Add-Member -inputObject $file -type NoteProperty -name "path" -value $childItem.FullName
   Add-Member -inputObject $file -type NoteProperty -name "name" -value $childItem.Name
   Add-Member -inputObject $file -type NoteProperty -name "type" -value $childItem.Extension
   Add-Member -inputObject $file -type NoteProperty -name "bytes" -value $childItem.Length
   Add-Member -inputObject $file -type NoteProperty -name "creationTime" -value ($childItem.CreationTime).ToString()
   $files += $file
 return $files

$files = Search-NestedDirectory "C:\" @()

$files | Export-Csv -path "files.csv" -noTypeInformation

Tuesday, January 11, 2011

Send E-Mail from PowerShell

One of the easiest and most common things I have my PowerShell scripts do is inform me and/or interested parties that they have completed a task and deliver some sort of information back via e-mail such as an Excel document. Below is a function I wrote that I "dot source" in my PowerShell scripts. You can take this code and test it out by replacing the information in the Parameters section. You may want to add some error detection in your script to ensure that at least the To:, CC: or BCC: is populated, ping the mail server and ensure that there is a subject and message before you pass the variables to the function. No error checking is done within the function.
Function Send-EMail($from,$to,$cc,$bcc,$subject,$priority,$html,$message,$mailAttachments,$smtpServer) {
  $mailMessage = New-Object System.Net.Mail.MailMessage

  $mailMessage.From = $from

  foreach($person in $to) {

  if($cc -ne $null) {
    foreach($person in $cc) {

  if($bcc -ne $null) {
    foreach($person in $bcc) {

  if($priority -eq "Normal" -or $priority -eq "Low" -or $priority -eq "High") {
    $mailMessage.Priority = $Priority
  } else {
    $mailMessage.Priority = "Normal"

  $mailMessage.Subject = $subject
  $mailMessage.Body = ($message | Out-String)
  $mailMessage.IsBodyHTML = $html

  if($mailAttachments.count -gt 0) {
    foreach($mailAttachment in $mailAttachments) {
    if(Test-Path -path $mailAttachment) {
      $attachment = New-Object System.Net.Mail.Attachment $mailAttachment

  $smtpClient = New-Object Net.Mail.SmtpClient($smtpServer)

# Parameters
$smtpServer = ""

$from = "yourscript@yourdomain.local"
$to = @("you@yourdomain.local","admin1@yourdomain.local","interestedparty@theirdomain.local")
$cc = @("someoneelse@yourdomain.local")
$bcc = @("yourboss@yourdomain.local","yourclient@theirdomain.local")

$subject = "E-Mail from your PowerShell Script"

$priority = "Low" # 'High' & 'Normal'

$html = $false # Or $true
# I like to use an array to think of each line of the message as an element in the array
$message = @()
$message += "Dear Administrator,"
$message += ""
$message += "I want you to know that thing you wanted me to do is now complete. Here is your report"
$message += "containing the stuff you need to know."
$message += ""
$message += "Your automated pal,"
$message += "  Scheduled Task"

$mailAttachments = @("\\\reports\ThatStuffYouNeededToKnow.xlsx")

Send-EMail $from $to $cc $bcc $subject $priority $html $message $mailAttachments $smtpServer

Monday, January 10, 2011

Convert Active Directory Object objectSid attribute to String in PowerShell

Here is a quick and simple solution to a problem that comes up from time to time. I need to know the string value of an Active Directory object's security identifier (sid) for a comparison, usually on a non-Windows system. Here is how I generate that string.
$objectSid = [byte[]]$activeDirectoryObject.objectSid 
$sid = New-Object System.Security.Principal.SecurityIdentifier($objectSid,0) 
$sidString = ($sid.value).ToString()

Friday, January 7, 2011

Active Directory Epoch vs. PowerShell Epoch in Attribute Based LDAP Queries

Active Directory stores time as the number of 100-nanosecond intervals (ticks) that have elapsed since midnight, January 1, 1601 UTC (GMT) in attributes such as LastLogon, LastLogonTimestamp, LastPwdSet and AccountExpires. PowerShell's epoch begins midnight, January 1, 0001 UTC. This leads to a little problem when you want to search objects by time-based attributes via an LDAP query in Active Directory using PowerShell. You can't simply say "(AccountExpires<=" + ((Get-Date).AddDays(-90)).Ticks + ")" to search for user objects that expired 90 days ago. You have to calculate the offset between Active Directory epoch and PowerShell epoch. To do this, you need to store the Active Directory epoch as  DateTime in PowerShell and subract its Ticks from the DateTime Ticks you are interested in querying against. Commonly, you will find scripts on the Internet that do the same process but have you return all objects and loop them through an if/then statement to perform the same process on a calculated attribute. Doing this ticks-based query upfront is more efficient and returns results much faster -- particularly in domains with large numbers of objects. Here is an example of an LDAP filter that could be used to find user objects that have expired 90 or more days ago.
$activeDirectoryEpoch = (Get-Date "1601-1-1T00:00:00-00:00").ToUniversalTime()
$expirationDate = (((Get-Date).AddDays(-90)).ToUniversalTime()).Ticks - $activeDirectoryEpoch.ticks
$ldapFilter = "(&(objectCategory=person)(objectClass=user)(AccountExpires<=$expirationDate)(!AccountExpires=9223372036854775807)(!AccountExpires=0))"

Thursday, January 6, 2011

Determine the Last Day of the Month in PowerShell

I run into situations where I need to export mailboxes using Export-Mailbox and need to break up the returned PST files by month. To do this, I need to know the start of the month (easy) and the end of the month (not as easy). This is how I generate that DateTime variable.
$date = "02/15/2008" # Using a date in a leap year for fun
$firstDayOfMonth = Get-Date ((((Get-Date $date).Month).ToString() + "/1/" + ((Get-Date $date).Year).ToString() + " 00:00:00"))
$lastDayOfMonth = ((Get-Date ((((Get-Date $firstDayOfMonth).AddMonths(1)).Month).ToString() + "/1/" + (((Get-Date $firstDayOfMonth).AddMonths(1)).Year).ToString()))) - (New-TimeSpan -seconds 1)
Write-Host ("-StartDate " + (Get-Date $firstDayOfMonth -format d) + " -EndDate " + (Get-Date $lastDayOfMonth -format d))

Wednesday, January 5, 2011

Dealing with the foreignSecurityPrincipal Object Class

If you run multiple forests in your Active Directory environment, you may grant trust between forests allowing you to populate groups or assign permissions with accounts from the trusted forest. If you are enumerating those groups or auditing those permissions using PowerShell, you will find that you will need to translate the foreignSecurityPrincipal object class returned by the trusting forest group or security descriptor. Below is a quick code sample how to obtain enough information to query the trusted domain for detailed information about the trusted object. Knowing the domain and sAMAccountName, one should be able to perform an ADSI search in the trusted forest.
$securityPrincipalObject = New-Object System.Security.Principal.SecurityIdentifier($
($domain, $sAMAccountName) = ($securityPrincipalObject.Translate([System.Security.Principal.NTAccount]).value).Split("\")

Tuesday, January 4, 2011

Get Object Active Directory Domain from distinguishedName

$distinguishedName = "CN=Doe\, John,OU=Advertising,OU=User,OU=New York,OU=Accounts,DC=usa,DC=corp,DC=foobar,DC=local"
$objectDomain = ((($distinguishedName -replace "(.*?)DC=(.*)",'$2') -replace "DC=","") -replace ",",".")

Monday, January 3, 2011

PowerShell, Windows Server 2008 & Excel 2010 64 Bit Scheduled Task Error

Are you trying to use PowerShell to generate a spreadsheet via a Scheduled Task using a 64 bit version of Excel 2010 on Windows Server 2008 64 bit and get the following error?

'Exception calling "SaveAs" with "1" argument(s): "SaveAs method of Workbook class failed"'

Yet, you do not get this error when running the script from the PowerShell command line? Frustrating isn't it? Quit banging your head against your desk and make sure this path is valid via PowerShell.
Test-Path -path $env:windir\System32\config\systemprofile\Desktop
If the return from this command is "False", run the following to create the directory.
New-Item -path $env:windir\System32\config\systemprofile\Desktop -type Directory