Friday, February 25, 2011

IP Address to Binary

When I was teaching myself subnetting some time ago, I wrote a perl script to assist in learning the conversion. There are many ways to do this but I wanted a method to help me learn to do the conversion in my head. This is why the subroutine is written out as collection of if than statements to represent the bits in the octet.
#!/usr/bin/perl
# use: ./ip2binary.pl 192.168.1.1
use strict;

my $ip = $ARGV[0];

if($ip =~ /\b(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\b/) {
 my @binary;
 my @octets = split(/\./,$ip);

 foreach my $octet (@octets) {
  push (@binary, return_binary($octet));
 }

 print join(".",@binary) . "\n";
} else {
 print "$ip is not a valid IP\n";
}

exit;

sub return_binary {
 my $decimal = $_[0];
 my @binary;
 if($decimal >= 128) {
  $binary[0] = 1;
  $decimal -= 128;
 } else {
  $binary[0] = 0;
 }

 if($decimal >= 64) {
  $binary[1] = 1;
  $decimal -= 64;
 } else {
  $binary[1] = 0;
 }

 if($decimal >= 32) {
  $binary[2] = 1;
  $decimal -= 32;
 } else {
  $binary[2] = 0;
 }

 if($decimal >= 16) {
  $binary[3] = 1;
  $decimal -= 16;
 } else {
  $binary[3] = 0;
 }

 if($decimal >= 8) {
  $binary[4] = 1;
  $decimal -= 8;
 } else {
  $binary[4] = 0;
 }

 if($decimal >= 4) {
  $binary[5] = 1;
  $decimal -= 4;
 } else {
  $binary[5] = 0;
 }

 if($decimal >= 2) {
  $binary[6] = 1;
  $decimal -= 2;
 } else {
  $binary[6] = 0;
 }

 if($decimal >= 1) {
  $binary[7] = 1;
  $decimal -= 1;
 } else {
  $binary[7] = 0;
 }

 return join("",@binary);

}

Thursday, February 24, 2011

Nagios Check for Forefront Client Security Signature Definitions

I have used Nagios for nearly 8 years and found it to be a great monitoring tool. Its flexible and allows you to monitor a heterogenous server environment with custom system checks tailored to your needs. For Windows Servers, the NSClient++ secure monitoring client allows you to go farther than simple ping, port and SNMP queries and execute scripts for monitoring services. Below is a VBScript I wrote to monitor the Forefront Client Security (FCS) Virus Signature freshness on a server by checking the age of the downloaded definition file. While you can monitor this directly in the FCS console, it becomes burdensome to monitor every product by its individual monitoring console. Integration into one monitoring platform saves a considerable amount of time and allows for straight forward escalation practices when trouble arises. One tool for evaluating your entire Enterprise health.
Const contHkeyLocalMachine = &H80000002
Const contReturnNormal = 0
Const contReturnWarning = 1
Const contReturnError = 2
Const contReturnUnknown = 3
 
strServer = "."
 
strToday = FormatDateTime(Date, 0)
strVirusDefinitionFilePath = NULL
Set objReg = GetObject("winmgmts:{impersonationLevel=impersonate}!\\" & strServer & "\root\default:StdRegProv")
 
strKeyPath = "SOFTWARE\Microsoft\Microsoft Forefront\Client Security\1.0\AM\Signature Updates"
strSignatureLocation = "SignatureLocation"
objReg.GetStringValue contHkeyLocalMachine,strKeyPath,strSignatureLocation,strVirusDefinitionFilePath
 
strVirusDefinitionFile = strVirusDefinitionFilePath & "\mpavdlta.vdm"
 
If strVirusDefinitionFile = "\mpavdlta.vdm" Then
 strReturnValue = contReturnError
 strResult = "Cannot determine location directory for mpavdlta.vdm from Registry. FCS might not have received its first update. Visit http://support.microsoft.com/kb/935934 to quickly resolve."
 WScript.Echo strResult
 WScript.Quit (strReturnValue)
End If
 
Set objFso = CreateObject("Scripting.FileSystemObject")
Set objFile = objFso.GetFile(strVirusDefinitionFile)
 
strVirusSignatureDate = FormatDateTime(objFile.DateCreated, 0)
 
intSignatureDefinitionDiff = DateDiff("d",strVirusSignatureDate,StrToday)
 
If intSignatureDefinitionDiff > 3 Then
 strReturnValue = contReturnError
ElseIf intSignatureDefinitionDiff > 1 Then
 strReturnValue = contReturnWarning
ElseIf intSignatureDefinitionDiff < 2 Then
 strReturnValue = contReturnNormal
Else
 strReturnValue = contReturnUnknown
End If
 
If intSignatureDefinitionDiff > 1 Then
 strResult = "Virus Signatures Last Updated: " & strVirusSignatureDate & " (" & intSignatureDefinitionDiff & " days ago)"
ElseIf intSignatureDefinitionDiff = 0 Then
 strResult = "Virus Signatures Last Updated: " & strVirusSignatureDate & " (Today)"
Else
 strResult = "Virus Signatures Last Updated: " & strVirusSignatureDate & " (" & intSignatureDefinitionDiff & " day ago)"
End If
 
WScript.Echo strResult
WScript.Quit (strReturnValue)

Wednesday, February 23, 2011

Export Manager's Nested Direct Reports

In the code sample below, I create an export in two flavors for the nested direct reports of managers. By taking a text file listing the e-mail addresses of user objects in a forest, this code proceeds to drill recursively through the mutli-valued string directReports attribute of the object until it reaches a nested user object with that attribute set to null. After obtaining all the distinguished names, the code proceeds to perform LDAP lookups to obtain relevant attribute data. At the conclusion, it saves two files, a comma separated values text file with all the relevant attributes and an HTML file with a subset of those attributes.

The concept for this script was done before PowerShell V2 was released and provided better typing including 64 bit integers. Like in my older Perl and VBScript code dealing with Active Directory attributes that stored 64 bit integers like accountExpires, it required manipulating the return data in order to perform comparisons. The code below contains a Function written by Adam Weigert which has saved me the time translating the same conversion I've done before in Perl and VBScript. A link to his blog entry on the topic is commented in the code for your review.
param([string]$file)

Function Get-DirectReports($objectDn, $distinguishedNames) {
 $distinguishedNames += $objectDn
 $managerObject = Get-ActiveDirectoryObject $objectDn
 if($managerObject.directReports.count -gt 0) {
  foreach($directReport in $managerObject.directReports) {
   $distinguishedNames = Get-DirectReports $directReport $distinguishedNames
  }
 }
 return $distinguishedNames
}
Function Read-UserStatus($userFlags) {
 if ($userFlags -band 0x0002) {
  $enabled = $false
    } else {
  $enabled = $true
 } 
 return $enabled
}
Function Convert-ADSLargeInteger([object]$adsLargeInteger) { # http://weblogs.asp.net/adweigert/archive/2007/03/23/powershell-convert-active-directory-iadslargeinteger-to-system-int64.aspx
 $highPart = $adsLargeInteger.GetType().InvokeMember("HighPart", [System.Reflection.BindingFlags]::GetProperty, $null, $adsLargeInteger, $null)
 $lowPart  = $adsLargeInteger.GetType().InvokeMember("LowPart",  [System.Reflection.BindingFlags]::GetProperty, $null, $adsLargeInteger, $null)

 $bytes = [System.BitConverter]::GetBytes($highPart)
 $temp   = [System.Byte[]]@(0,0,0,0,0,0,0,0)
 [System.Array]::Copy($bytes, 0, $temp, 4, 4)
 $highPart = [System.BitConverter]::ToInt64($temp, 0)

 $bytes = [System.BitConverter]::GetBytes($lowPart)
 $lowPart = [System.BitConverter]::ToUInt32($bytes, 0)
 
 return $lowPart + $highPart
}
Function Get-LocalDomainController($objectDomain) {
 return ([System.DirectoryServices.ActiveDirectory.ActiveDirectorySite]::GetComputerSite()).Servers | Where-Object { $_.Domain.Name -eq $objectDomain } | ForEach-Object { $_.Name } | Select-Object -first 1
}
  
Function Get-ObjectADDomain($distinguishedName) {
 return ((($distinguishedName -replace "(.*?)DC=(.*)",'$2') -replace "DC=","") -replace ",",".")
}
  
Function Get-ActiveDirectoryObject($distinguishedName) {
 return [ADSI]("LDAP://" + (Get-LocalDomainController (Get-ObjectADDomain $distinguishedName)) + "/" + ($distinguishedName -replace "/","\/"))
}
#--------------------------------------------------------------------------------------------------#
Set-Variable -name forestRootDn -option Constant -value ([ADSI]("LDAP://" + (([System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()).name) + "/rootDSE")).defaultNamingContext
#--------------------------------------------------------------------------------------------------#
if(![bool]$file) {
 Write-Host "You are missing the `"-file`" command line argument" -foregroundColor Red
 Write-Host ""
 Write-Host "This is the file that contains the list of employee e-mail addresses"
 Write-Host ""
 while((![bool]$file)) {
  $file = Read-Host -prompt "`tFile"
 }
 Write-Host ""
}
if(!(Test-Path -path $file)) {
 Write-Host "Unable to locate: $file" -foregroundColor Red
 Write-Host ""
 Write-Host "Please review the correct path"
 Write-Host ""
 while(!(Test-Path -path $file)) {
  $file = Read-Host -prompt "`tFile"
 }
 Write-Host ""
}
   
$answer = $null
while($answer -ne "Y" -and $answer -ne "N") {
 $answer = Read-Host -prompt "Are you sure you want to use $file (Y/N)?"
}
   
if($answer -eq "N") {
 Write-Host ""
 Write-Host "Quiting..." -foregroundColor Yellow
 exit
}
 
$outputFile = (((Get-Item -path $file).name).Replace(((Get-Item -path $file).extension),".csv"))
 
$managers = Get-Content -path $file

$objectConnection = New-Object -comObject "ADODB.Connection"
$objectCommand = New-Object -comObject "ADODB.Command"
$objectConnection.Open("Provider=ADsDSOObject;")
$objectCommand.ActiveConnection = $objectConnection

$distinguishedNames = @()
$users = @()

foreach($manager in $managers) {

 $ldapBase = "GC://$forestRootDn"
 $ldapAttr = "distinguishedName"
 $ldapScope = "subtree"
 $ldapFilter = "(&(objectClass=user)(ProxyAddresses=smtp:$manager))"
 $ldapQuery= "<$ldapBase>;$ldapFilter;$ldapAttr;$ldapScope"
 $objectCommand.CommandText = $ldapQuery
 $objectRecordSet = $objectCommand.Execute()

 while(!$objectRecordSet.EOF) {
  $distinguishedNames = Get-DirectReports $objectRecordSet.Fields.Item('distinguishedName').Value $distinguishedNames
  $objectRecordSet.MoveNext()
 }
}

foreach($distinguishedName in $distinguishedNames) {
 $userObject = Get-ActiveDirectoryObject $distinguishedName

 if($userObject.objectClass -eq "User") {
  $accountExpires = (Convert-ADSLargeInteger $userObject.accountExpires[0])

  if($accountExpires -ne 0 -and $accountExpires -ne 9223372036854775807) {
   $account_expiration = (Get-Date ($userObject.psbase.invokeget("AccountExpirationDate")) -Format d)
  } else {
   $account_expiration = "N/A"
  }
 
  if($userObject.manager) {
   $manager_object = Get-ActiveDirectoryObject $userObject.manager
   if($manager_object.displayName) {
    $manager = ($manager_object.displayName).ToString()
   } else {
    $manager = ($manager_object.name).ToString()
   }
  } else {
   $manager = "N/A"
  }
  
  $user = New-Object -typeName PSObject
  Add-Member -inputObject $user -type NoteProperty -name "domain" -value ((Get-ObjectADDomain $userObject.distinguishedName).Split(".")[0]).ToUpper()
  Add-Member -inputObject $user -type NoteProperty -name "sAMAccountName" -value (($userObject.sAMAccountName).ToString()).ToLower()
  Add-Member -inputObject $user -type NoteProperty -name "givenName" -value ($userObject.givenName).ToString()
  Add-Member -inputObject $user -type NoteProperty -name "sn" -value ($userObject.sn).ToString()
  Add-Member -inputObject $user -type NoteProperty -name "displayName" -value ($userObject.displayName).ToString()
  Add-Member -inputObject $user -type NoteProperty -name "mail" -value ($userObject.mail).ToString()
  Add-Member -inputObject $user -type NoteProperty -name "title" -value ($userObject.title).ToString()
  Add-Member -inputObject $user -type NoteProperty -name "department" -value ($userObject.department).ToString()
  Add-Member -inputObject $user -type NoteProperty -name "manager" -value $manager
  Add-Member -inputObject $user -type NoteProperty -name "streetAddress" -value (($userObject.streetAddress).ToString() -Replace "`r`n",", ")
  Add-Member -inputObject $user -type NoteProperty -name "l" -value ($userObject.l).ToString()
  Add-Member -inputObject $user -type NoteProperty -name "st" -value ($userObject.st).ToString()
  Add-Member -inputObject $user -type NoteProperty -name "postalCode" -value ($userObject.postalCode).ToString()
  Add-Member -inputObject $user -type NoteProperty -name "c" -value ($userObject.c).ToString()
  Add-Member -inputObject $user -type NoteProperty -name "telephoneNumber" -value ($userObject.telephoneNumber).ToString()
  Add-Member -inputObject $user -type NoteProperty -name "accountExpiration" -value $account_expiration
  Add-Member -inputObject $user -type NoteProperty -name "enabled" -value (Read-UserStatus $userObject.userAccountControl[0])
  Add-Member -inputObject $user -type NoteProperty -name "organizationalUnit" -value ($userObject.psBase.parent.distinguishedName).ToString()
  $users += $user
 }
}

$users | Export-Csv -path $outputFile -noTypeInformation
$users | ConvertTo-Html -property givenName,sn,mail,telephoneNumber,title,department -title "Important Contacts" > "contacts.html"

Tuesday, February 22, 2011

Scheduling Tasks in PowerShell for Windows Server 2008 -- Part 2: Weekly Tasks

In my previous blog post, "Scheduling Tasks in PowerShell for Windows Server 2008 -- Part 1: Monthly Task", I laid the groundwork for the various ways you can schedule tasks in Windows Server 2008. This installment of the series shows how to create a weekly task with multiple triggers. In the code sample below, I schedule a report to run on Monday (2), Wednesday (8) and Thursday (16) at 4:30 pm local time to the server. To extend this example further, you could use a hash table to establish a different time of day for each of the day of the week task triggers.
# Parameters to modify
$taskName = "Export-TriWeeklyDataDeobfuscationReport.ps1"
$taskWorkingDirectory = "C:\PowerShell"
$taskAuthor = "ad\myaccount"
$taskDescription = "The Tri-Weekly Data Deobfuscation Report"
$taskSecurityPrincipal = "ad\myaccount"
$taskShedulerTaskFolder = "\MyTasks"
$startTime = (Get-Date "02/28/2011 16:30:00" -Format s)
$daysOfWeek = @("2","8","16") # http://msdn.microsoft.com/en-us/library/aa381905(v=VS.85).aspx

# Would like to use -asSecureString but RegisterTaskDefinition does not accept it
# Look over your shoulder before typing
$password = Read-Host -prompt "$taskSecurityPrincipal Password"

# The meaty parts

$taskService = New-Object -ComObject Schedule.Service
$taskService.Connect()

$rootFolder = $taskService.GetFolder($taskShedulerTaskFolder)

$taskDefinition = $taskService.NewTask(0)

$registrationInformation = $taskDefinition.RegistrationInfo
$registrationInformation.Description = $taskDescription
$registrationInformation.Author = $taskAuthor

$taskPrincipal = $taskDefinition.Principal
$taskPrincipal.LogonType = 1
$taskPrincipal.UserID = $taskSecurityPrincipal
$taskPrincipal.RunLevel = 0

$taskSettings = $taskDefinition.Settings
$taskSettings.StartWhenAvailable = $true
$taskSettings.RunOnlyIfNetworkAvailable = $true
$taskSettings.Priority = 7
$taskSettings.ExecutionTimeLimit = "PT2H"

$taskTriggers = $taskDefinition.Triggers

foreach($dayOfWeek in $daysOfWeek) {
 $executionTrigger = $taskTriggers.Create(3) 
 $executionTrigger.DaysOfWeek = $dayOfWeek
 $executionTrigger.StartBoundary = $startTime
}

$taskAction = $taskDefinition.Actions.Create(0)
$taskAction.Path = "%SystemRoot%\system32\WindowsPowerShell\v1.0\powershell.exe"
$taskAction.Arguments = "-command `"$taskWorkingDirectory\$taskName`""
$taskAction.WorkingDirectory = $taskWorkingDirectory

# 6 == Task Create or Update
# 1 == Password must be supplied at registration
$rootFolder.RegisterTaskDefinition($taskName, $taskDefinition, 6, $taskSecurityPrincipal, $password, 1)

# Since we captured this in plain text I am going to nuke the value
# Not 100% security. Close the PowerShell command window.
Clear-Variable -name password

Thursday, February 17, 2011

BlackBerry User Report

This is a report that I run on a regular basis to keep track of BlackBerry users worldwide for review. The BES administration tools can provide decent reports but it doesn't provide all the information I want to have or need to pass on to others. It marries information from the BlackBerry database and Active Directory using an e-mail address as the primary key between the two data sources. This report is especially useful when you have BlackBerry servers scattered at different data centers because of the MAPI latency requirements of the BES. In large BES environments, expect this script to take a generous amount of time to complete. You will need to ensure that the security context that this script runs under has read rights to the BES databases that it connects. There are definitely areas for improvement in the data returns. I have sort of punted on the formatting of the BlackBerry phone number. I am primarily focused on 10 digit numbers found in the USA and not formatting for foreign numbers. You can take advantage of the c attribute to tweak that formatting. I will update the code in the future to add this.

Over the years, I have massed a list of vendor IDs that I use when there is missing or odd data from the home network column in the BlackBerry database. Having this information gives you the flexibility to ignore the home network and use the vendor ID for reporting. Please leave a comment if you see missing data or updates for these vendor IDs.
Function Get-LocalDomainController($objectDomain) {
 return ([System.DirectoryServices.ActiveDirectory.ActiveDirectorySite]::GetComputerSite()).Servers | Where-Object { $_.Domain.Name -eq $objectDomain } | ForEach-Object { $_.Name } | Select-Object -first 1
}
 
Function Get-ObjectADDomain($distinguishedName) {
 return ((($distinguishedName -replace "(.*?)DC=(.*)",'$2') -replace "DC=","") -replace ",",".")
}
 
Function Get-ActiveDirectoryObject($distinguishedName) {
 return [ADSI]("LDAP://" + (Get-LocalDomainController (Get-ObjectADDomain $distinguishedName)) + "/" + ($distinguishedName -replace "/","\/"))
}
Function Get-CarrierNetwork($carrier) {
 if($carrier -eq "100") { $network = "T-Mobile US"
 } elseif($carrier -eq "101") { $network = "Cingular Wireless"
 } elseif($carrier -eq "102") { $network = "AT&T Wireless"
 } elseif($carrier -eq "103") { $network = "Nextel"
 } elseif($carrier -eq "104") { $network = "Sprint PCS"
 } elseif($carrier -eq "105") { $network = "Verizon Wireless"
 } elseif($carrier -eq "106") { $network = "Alltel"
 } elseif($carrier -eq "107") { $network = "Rogers AT&T"
 } elseif($carrier -eq "108") { $network = "Microcell"
 } elseif($carrier -eq "109") { $network = "Bell Mobility"
 } elseif($carrier -eq "110") { $network = "BT Cellnet"
 } elseif($carrier -eq "111") { $network = "O2 Germany"
 } elseif($carrier -eq "112") { $network = "Digifone"
 } elseif($carrier -eq "113") { $network = "Telfort"
 } elseif($carrier -eq "114") { $network = "T-Mobile Germany Austria"
 } elseif($carrier -eq "115") { $network = "Tim Italy"
 } elseif($carrier -eq "116") { $network = "Hutchison"
 } elseif($carrier -eq "117") { $network = "Bouygues Telecom"
 } elseif($carrier -eq "118") { $network = "Vodafone SFR France"
 } elseif($carrier -eq "119") { $network = "Orange France"
 } elseif($carrier -eq "120") { $network = "Vodafone UK Netherlands"
 } elseif($carrier -eq "121") { $network = "Telcel Mexico"
 } elseif($carrier -eq "122") { $network = "Telstra"
 } elseif($carrier -eq "123") { $network = "T-Mobile UK"
 } elseif($carrier -eq "124") { $network = "Vodafone Germany"
 } elseif($carrier -eq "125") { $network = "O2 UK Ireland Isle Of Man Netherlands"
 } elseif($carrier -eq "126") { $network = "Telus"
 } elseif($carrier -eq "127") { $network = "Smart"
 } elseif($carrier -eq "128") { $network = "Starhub"
 } elseif($carrier -eq "129") { $network = "Telefonica Spain"
 } elseif($carrier -eq "130") { $network = "Vodafone Switzerland Swisscom"
 } elseif($carrier -eq "131") { $network = "Cable Wireless West Indies"
 } elseif($carrier -eq "132") { $network = "Vodafone Italy"
 } elseif($carrier -eq "133") { $network = "Vodafone Spain"
 } elseif($carrier -eq "134") { $network = "T-Mobile Netherlands"
 } elseif($carrier -eq "135") { $network = "Cincinnati Bell"
 } elseif($carrier -eq "136") { $network = "Telefonica Mexico"
 } elseif($carrier -eq "137") { $network = "Vodafone Austria"
 } elseif($carrier -eq "138") { $network = "Vodafone Australia Fiji"
 } elseif($carrier -eq "139") { $network = "Vodafone Ireland"
 } elseif($carrier -eq "140") { $network = "Telenor Sweden"
 } elseif($carrier -eq "141") { $network = "CSL"
 } elseif($carrier -eq "142") { $network = "Orange UK"
 } elseif($carrier -eq "143") { $network = "Vodafone New Zealand"
 } elseif($carrier -eq "144") { $network = "Singtel"
 } elseif($carrier -eq "145") { $network = "Globe"
 } elseif($carrier -eq "146") { $network = "Optus"
 } elseif($carrier -eq "147") { $network = "Orange Be Mobistar"
 } elseif($carrier -eq "148") { $network = "Vodafone Hungary"
 } elseif($carrier -eq "149") { $network = "Bharti"
 } elseif($carrier -eq "150") { $network = "KPN NL"
 } elseif($carrier -eq "151") { $network = "Wind Hellas Tim Greece"
 } elseif($carrier -eq "152") { $network = "Vodafone Belgium"
 } elseif($carrier -eq "153") { $network = "Vodafone Portugal"
 } elseif($carrier -eq "154") { $network = "Tim Brazil"
 } elseif($carrier -eq "155") { $network = "BT-Mobile"
 } elseif($carrier -eq "156") { $network = "Earthlink"
 } elseif($carrier -eq "157") { $network = "Aether"
 } elseif($carrier -eq "158") { $network = "E Plus"
 } elseif($carrier -eq "159") { $network = "Base"
 } elseif($carrier -eq "160") { $network = "Dobson Communications"
 } elseif($carrier -eq "161") { $network = "Vodafone Egypt"
 } elseif($carrier -eq "162") { $network = "Orange Switzerland"
 } elseif($carrier -eq "163") { $network = "Rim Wlan"
 } elseif($carrier -eq "164") { $network = "T-Mobile Suncom"
 } elseif($carrier -eq "165") { $network = "Maxis"
 } elseif($carrier -eq "166") { $network = "Vodafone Denmark TDC"
 } elseif($carrier -eq "167") { $network = "Vodafone Singapore M1"
 } elseif($carrier -eq "168") { $network = "Vodacom South Africa"
 } elseif($carrier -eq "169") { $network = "T-Mobile Poland"
 } elseif($carrier -eq "170") { $network = "T-Mobile Czech"
 } elseif($carrier -eq "171") { $network = "T-Mobile Hungary"
 } elseif($carrier -eq "172") { $network = "AT&T Sprint"
 } elseif($carrier -eq "173") { $network = "Mtn South Africa"
 } elseif($carrier -eq "174") { $network = "Tim Chile Entel PCS"
 } elseif($carrier -eq "175") { $network = "Orange Spain"
 } elseif($carrier -eq "176") { $network = "Vodafone Smartone Hong Kong"
 } elseif($carrier -eq "177") { $network = "TCS Telecommunication Systems"
 } elseif($carrier -eq "178") { $network = "Avea"
 } elseif($carrier -eq "179") { $network = "Fast 100"
 } elseif($carrier -eq "180") { $network = "Turkcell"
 } elseif($carrier -eq "181") { $network = "Partner Communications"
 } elseif($carrier -eq "183") { $network = "Orange Romania"
 } elseif($carrier -eq "186") { $network = "Telkomsel"
 } elseif($carrier -eq "188") { $network = "Vodafone Greece"
 } elseif($carrier -eq "189") { $network = "United States Cellular Corp"
 } elseif($carrier -eq "190") { $network = "Mobilink"
 } elseif($carrier -eq "191") { $network = "Velocita Wireless"
 } elseif($carrier -eq "192") { $network = "Vodafone Croatia"
 } elseif($carrier -eq "193") { $network = "Vodafone Slovenia"
 } elseif($carrier -eq "194") { $network = "Vodafone Luxembourg"
 } elseif($carrier -eq "195") { $network = "Vodafone Iceland"
 } elseif($carrier -eq "196") { $network = "Vodafone Fiji"
 } elseif($carrier -eq "197") { $network = "Vodafone Romania"
 } elseif($carrier -eq "198") { $network = "Vodafone Czech"
 } elseif($carrier -eq "199") { $network = "Vodafone Bahrain"
 } elseif($carrier -eq "200") { $network = "Vodafone Kuwait"
 } elseif($carrier -eq "201") { $network = "T-Mobile Croatia"
 } elseif($carrier -eq "202") { $network = "T-Mobile Slovakia"
 } elseif($carrier -eq "203") { $network = "Nortel"
 } elseif($carrier -eq "204") { $network = "China Mobile"
 } elseif($carrier -eq "205") { $network = "Movilnet"
 } elseif($carrier -eq "209") { $network = "Sympac"
 } elseif($carrier -eq "210") { $network = "Personal Argentina"
 } elseif($carrier -eq "212") { $network = "Etisalat UAE"
 } elseif($carrier -eq "213") { $network = "Cbeyond"
 } elseif($carrier -eq "214") { $network = "AMX"
 } elseif($carrier -eq "215") { $network = "Telefonica Venezuela"
 } elseif($carrier -eq "216") { $network = "Telefonica Brazil"
 } elseif($carrier -eq "217") { $network = "Orange Romania"
 } elseif($carrier -eq "218") { $network = "Ktpowertel Korea"
 } elseif($carrier -eq "219") { $network = "Rolling Stones"
 } elseif($carrier -eq "220") { $network = "Docomo"
 } elseif($carrier -eq "222") { $network = "Vodafone Bulgaria"
 } elseif($carrier -eq "223") { $network = "Nextel International"
 } elseif($carrier -eq "224") { $network = "PCCW Sunday"
 } elseif($carrier -eq "225") { $network = "Hawaiian Telcom Credo Mobile"
 } elseif($carrier -eq "226") { $network = "Verizon Mvno"
 } elseif($carrier -eq "227") { $network = "Mobily"
 } elseif($carrier -eq "228") { $network = "BWA"
 } elseif($carrier -eq "229") { $network = "O2 Czech Republic"
 } elseif($carrier -eq "230") { $network = "Hutchison India"
 } elseif($carrier -eq "231") { $network = "Celcom"
 } elseif($carrier -eq "234") { $network = "Dialog"
 } elseif($carrier -eq "235") { $network = "XL"
 } elseif($carrier -eq "236") { $network = "Reliance"
 } elseif($carrier -eq "237") { $network = "Verizon Wireless Wholesale"
 } elseif($carrier -eq "238") { $network = "Vodafone Turkey"
 } elseif($carrier -eq "239") { $network = "Telefonica Morocco Meditel"
 } elseif($carrier -eq "240") { $network = "Indosat"
 } elseif($carrier -eq "241") { $network = "Alcatel Shanghai Bell"
 } elseif($carrier -eq "245") { $network = "3 UK Italy Sweden Denmark Austria Ireland"
 } elseif($carrier -eq "247") { $network = "Vodafone Essar"
 } elseif($carrier -eq "248") { $network = "Centennial Wireless"
 } elseif($carrier -eq "250") { $network = "T-Mobile Austria"
 } elseif($carrier -eq "254") { $network = "OI Brazil"
 } elseif($carrier -eq "255") { $network = "Telecom New Zealand"
 } elseif($carrier -eq "258") { $network = "Hutchinson 3G Australia"
 } elseif($carrier -eq "259") { $network = "Cable & Wireless Trinidad Tobago"
 } elseif($carrier -eq "268") { $network = "Bmobile"
 } elseif($carrier -eq "269") { $network = "Tata Teleservices India"
 } elseif($carrier -eq "271") { $network = "T-Mobile Croatia"
 } elseif($carrier -eq "273") { $network = "BT Italy"
 } elseif($carrier -eq "274") { $network = "1&1"
 } elseif($carrier -eq "277") { $network = "MTS Mobility"
 } elseif($carrier -eq "278") { $network = "Virgin Mobile"
 } elseif($carrier -eq "280") { $network = "Orange Slovakia"
 } elseif($carrier -eq "282") { $network = "Taiwan Mobile"
 } elseif($carrier -eq "285") { $network = "Orange Austria"
 } elseif($carrier -eq "286") { $network = "Vodafone Malta"
 } elseif($carrier -eq "288") { $network = "Base Jim Mobile"
 } elseif($carrier -eq "295") { $network = "CMCC Peoples"
 } elseif($carrier -eq "298") { $network = "Digitel Wireless"
 } elseif($carrier -eq "299") { $network = "SK Telecom"
 } elseif($carrier -eq "300") { $network = "Solo Mobile"
 } elseif($carrier -eq "301") { $network = "Carphone Warehouse"
 } elseif($carrier -eq "302") { $network = "20:20 Mobile Group"
 } elseif($carrier -eq "308") { $network = "XL Indonesia"
 } elseif($carrier -eq "309") { $network = "Fido Solutions"
 } elseif($carrier -eq "310") { $network = "Wind Italy"
 }
 return $network
}
#--------------------------------------------------------------------------------------------------#
Set-Variable -name forestRootDn -option Constant -value ([ADSI]("LDAP://" + (([System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()).name) + "/rootDSE")).defaultNamingContext
# Modify the array below to include your SQL servers or local SQL instance on a BES
Set-Variable -name sqlServers -option Constant -value @("east-coast-sql.usa.ad.mydomain.local","west-coast-sql.usa.ad.mydomain.local","remote-bes-server.japan.ad.mydomain.local")
#--------------------------------------------------------------------------------------------------#
$blackberryUsers = @()

$objectConnection = New-Object -comObject "ADODB.Connection"
$objectCommand = New-Object -comObject "ADODB.Command"
$objectConnection.Open("Provider=ADsDSOObject;")
$objectCommand.ActiveConnection = $objectConnection

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

foreach($sqlServer in $sqlServers) {

 $sqlConnection = New-Object System.Data.SQLClient.SQLConnection
 $sqlConnection.ConnectionString = "server=$sqlServer;database=BESMgmt;trusted_connection=true;"
 $sqlConnection.Open()
 
 $sqlCommand = New-Object System.Data.SQLClient.SQLCommand
 $sqlCommand.Connection = $sqlConnection
 $sqlCommand.CommandText = "SELECT u.DisplayName as DisplayName, u.MailboxSMTPAddr as MailboxSMTPAddr, u.PIN as PIN, d.phonenumber as phonenumber, d.ModelName as ModelName, d.VendorID as VendorID, d.IMEI as IMEI, d.HomeNetwork as HomeNetwork, d.PasswordEnabled as PasswordEnabled, s.MachineName as MachineName FROM UserConfig u, SyncDeviceMgmtSummary d, ServerConfig s WHERE u.id = d.userconfigid and u.ServerConfigId = s.id"
 
 $sqlDataReader = $sqlCommand.ExecuteReader()
 
 if($sqlDataReader.HasRows -eq  $true) {
  while($sqlDataReader.Read()) {
   $displayName = $sqlDataReader.Item("DisplayName")
   $proxyAddress = $sqlDataReader.Item("MailboxSMTPAddr")
   $pin = $sqlDataReader.Item("PIN")
   $phoneNumber = $sqlDataReader.Item("phonenumber")
   $modelName = $sqlDataReader.Item("ModelName")
   $vendorId = $sqlDataReader.Item("VendorID")
   $imei = $sqlDataReader.Item("IMEI")
   $homeNetwork = $sqlDataReader.Item("homeNetwork")
   $passwordEnabled = $sqlDataReader.Item("PasswordEnabled")
   $blackberryServer = $sqlDataReader.Item("MachineName")

  if($passwordEnabled -eq 1) { 
    $passwordEnabled = $true
   } else { 
    $passwordEnabled = $false
   }
 
   $phoneNumber = $phoneNumber -Replace "^\+",""
 
   if(($phoneNumber).length -eq 10) {
    $phoneNumber = ("(" + ($phoneNumber).SubString(0,3) + ") " + ($phoneNumber).SubString(3,3) + "-" + ($phoneNumber).SubString(6,4))
   }
 
   if(($phoneNumber).length -eq 11 -and ($phoneNumber).SubString(0,1) -eq "1") {
    $phoneNumber = ("(" + ($phoneNumber).SubString(1,3) + ") " + ($phoneNumber).SubString(4,3) + "-" + ($phoneNumber).SubString(7,4))
   }
 
   if($homeNetwork -eq "" -or $homeNetwork -cmatch "vodafone") {
    $homeNetwork = Get-CarrierNetwork $vendorId
   }
 
   $ldapFilter = ("(&(objectCategory=person)(objectClass=user)(proxyAddresses=smtp:$proxyAddress))")
   $ldapQuery = "<$ldapBase>;$ldapFilter;$ldapAttr;$ldapScope"
 
   $objectCommand.CommandText = $ldapQuery
   $objectRecordSet = $objectCommand.Execute()
 
   while(!$objectRecordSet.EOF) {
    $domain = ((Get-ObjectADDomain $objectRecordSet.Fields.Item('distinguishedName').Value).Split(".")[0]).ToUpper()
    $exchangeServer = ((((((Get-ActiveDirectoryObject $objectRecordSet.Fields.Item('homeMDB').Value).psbase.parent).psbase.parent).psbase.parent).networkAddress[4]).ToString() -replace "ncacn_ip_tcp:","").ToLower()
    $sAMAccountName = $objectRecordSet.Fields.Item('sAMAccountName').Value
    $firstName = $objectRecordSet.Fields.Item('givenName').Value
    $lastName = $objectRecordSet.Fields.Item('sn').Value
    $telephoneNumber = $objectRecordSet.Fields.Item('telephoneNumber').Value
    $streetAddress = ($objectRecordSet.Fields.Item('streetAddress').Value -replace "`r`n",", ")
    $city = $objectRecordSet.Fields.Item('l').Value
    $state = $objectRecordSet.Fields.Item('st').Value
    $zipCode = $objectRecordSet.Fields.Item('postalCode').Value
    $country = $objectRecordSet.Fields.Item('c').Value
    $objectRecordSet.MoveNext()
   }

   $blackberryUser = New-Object -typeName PSObject
   Add-Member -inputObject $blackberryUser -type NoteProperty -name "domain" -value $domain
   Add-Member -inputObject $blackberryUser -type NoteProperty -name "sAMAccountName" -value $sAMAccountName
   Add-Member -inputObject $blackberryUser -type NoteProperty -name "firstName" -value $firstName
   Add-Member -inputObject $blackberryUser -type NoteProperty -name "lastName" -value $firstName
   Add-Member -inputObject $blackberryUser -type NoteProperty -name "displayName" -value $displayName
   Add-Member -inputObject $blackberryUser -type NoteProperty -name "emailAddress" -value $proxyAddress
   Add-Member -inputObject $blackberryUser -type NoteProperty -name "pin" -value $pin
   Add-Member -inputObject $blackberryUser -type NoteProperty -name "blackberryPhoneNumber" -value $phoneNumber
   Add-Member -inputObject $blackberryUser -type NoteProperty -name "officePhoneNumber" -value $telephoneNumber
   Add-Member -inputObject $blackberryUser -type NoteProperty -name "streetAddress" -value $streetAddress
   Add-Member -inputObject $blackberryUser -type NoteProperty -name "city" -value $city
   Add-Member -inputObject $blackberryUser -type NoteProperty -name "state" -value $state
   Add-Member -inputObject $blackberryUser -type NoteProperty -name "zipCode" -value $zipCode
   Add-Member -inputObject $blackberryUser -type NoteProperty -name "country" -value $country
   Add-Member -inputObject $blackberryUser -type NoteProperty -name "modelName" -value $modelName
   Add-Member -inputObject $blackberryUser -type NoteProperty -name "carrier" -value $homeNetwork
   Add-Member -inputObject $blackberryUser -type NoteProperty -name "imei" -value $imei
   Add-Member -inputObject $blackberryUser -type NoteProperty -name "passwordEnabled" -value $passwordEnabled
   Add-Member -inputObject $blackberryUser -type NoteProperty -name "exchangeServer" -value $exchangeServer
   Add-Member -inputObject $blackberryUser -type NoteProperty -name "blackberryServer" -value $blackberryServer 
   $blackberryUsers += $blackberryUser
  }
 }  else {
  Write-Warning "Unable to obtain BES data from $sqlServer"
  exit
 }
}
$blackberryUsers | Export-Csv -path "Blackberry Users.csv" -noTypeInformation

Wednesday, February 16, 2011

Removing Specific Messages from Exchange Mailboxes

In my previous blog post, "Search for Large Distribution Lists without Sending Restrictions", I talked about locking down distribution lists and mail-enabled security groups so only select employees can send to them. What do you do if you are too late and that malicious, departing employee has sent out their not-so-fond farewell to large group of recipients before their access was revoked (its all about timing!) and Human Resources has come to you to remove the message from all mailboxes that it was sent? Of course Human Resources has consulted with Legal and got their approval to have the message deleted before speaking with you. Because you have a distribution of e-mail, you have all the e-mail addresses that nasty-gram was sent.

In the code sample below, I take a text file containing those e-mail addresses and use a foreach loop to process the deletion of the e-mail using Export-Mailbox. The code itself is more than overkill as Export-Mailbox is doing all the real work but the surrounding code helps ensure that all elements you need are in place. Like any process that deletes data, I make sure I have a backup (because Legal and Human Resources will change their mind after you start) so the code will export the deleted message into a .pst file in a subfolder of the script directory using the e-mail address of the mailbox as the filename. I also store individual .xml reports in a subdirectory as well. If you have a large distribution to process, break up your list into multiple executions of the script to expedite the removals.

A few things to note about using Export-Mailbox to delete messages. One, Export-Mailbox requires a 32-bit operating system to execute. Two, if the dumpster is turned on, that content will be moved into the .pst file as well. Three, using the subject line is not the only method for discovering the messages you want to target. Review the Export-Mailbox command for other options.

And remember, this script deletes data! Use at your own risk! I might have the best of intentions but my skill may betray you. Test, test and further test before implementing this code in a production environment.
param([string]$file)
#--------------------------------------------------------------------------------------------------#
Function Load-ExchangeSnapin {
 $adminLoaded = $false
 $supportLoaded = $false
 $success = $false
 foreach($snapin in Get-PSSnapin) {
  if($snapin.name -eq "Microsoft.Exchange.Management.PowerShell.Admin") {
   $adminLoaded = $true
  }
  if($snapin.name -eq "Microsoft.Exchange.Management.PowerShell.Support") {
   $supportLoaded = $true
  }
 }
 if($adminLoaded -eq $false) {
  Add-PSSnapin -Name 'Microsoft.Exchange.Management.PowerShell.Admin'
 }
 if($supportLoaded -eq $false) {
  Add-PSSnapin -Name 'Microsoft.Exchange.Management.PowerShell.Support'
 }
 foreach($snapin in Get-PSSnapin) {
  if($snapin.name -eq "Microsoft.Exchange.Management.PowerShell.Admin") {
   $adminLoaded = $true
  }
  if($snapin.name -eq "Microsoft.Exchange.Management.PowerShell.Support") {
   $supportLoaded = $true
  }
 }
 if($adminLoaded -eq $true -and $supportLoaded -eq $true) {
  $success = $true
 }
 return $success
}
Function Unload-ExchangeSnapin {
 $adminLoaded = $false
 $supportLoaded = $false
 foreach($snapin in Get-PSSnapin) {
  if($snapin.name -eq "Microsoft.Exchange.Management.PowerShell.Admin") {
   $adminLoaded = $true
  }
  if($snapin.name -eq "Microsoft.Exchange.Management.PowerShell.Support") {
   $supportLoaded = $true
  }
 }
 if($adminLoaded -eq $true) {
  Remove-PSSnapin -Name 'Microsoft.Exchange.Management.PowerShell.Admin'
 }
 if($supportLoaded -eq $true) {
  Remove-PSSnapin -Name 'Microsoft.Exchange.Management.PowerShell.Support'
 }
}
#--------------------------------------------------------------------------------------------------#
#Modify the values here
Set-Variable -name subjectKeywords -option Constant -value "Screw you, I'm out of here!!!"
Set-Variable -name startDate -option Constant -value "02/16/2011"
Set-Variable -name endDate -option Constant -value "02/16/2011"
Set-Variable -name pstFolder -option Constant -value "$pwd\PST"
Set-Variable -name reportsFolder -option Constant -value "$pwd\Reports"
#--------------------------------------------------------------------------------------------------#
if(![bool]$file) {
 Write-Host "You are missing the `"-file`" command line argument" -foregroundColor Red
 Write-Host ""
 Write-Host "This is the file that contains the list of employee e-mail addresses"
 Write-Host ""
 while((![bool]$file)) {
  $file = Read-Host -prompt "`tFile"
 }
 Write-Host ""
}
if(!(Test-Path -path $file)) {
 Write-Host "Unable to locate: $file" -foregroundColor Red
 Write-Host ""
 Write-Host "Please review the correct path"
 Write-Host ""
 while(!(Test-Path -path $file)) {
  $file = Read-Host -prompt "`tFile"
 }
 Write-Host ""
}
  
$answer = $null
while($answer -ne "Y" -and $answer -ne "N") {
 $answer = Read-Host -prompt "Are you sure you want to use $file (Y/N)?"
}
  
if($answer -eq "N") {
 Write-Host ""
 Write-Host "Quiting..." -foregroundColor Yellow
 exit
}

if(!(Test-Path -path $pstFolder)) {
 New-Item -path $pstFolder -type directory | Out-Null
}
if(!(Test-Path -path $reportsFolder)) {
 New-Item -path $reportsFolder -type directory | Out-Null
}

if((Load-ExchangeSnapin) -eq $false) {
 Write-Warning "Could not load the Exchange Management Snapins"
 exit
}

foreach($emailAddress in (Get-Content -path $file)) {
 Export-Mailbox -Identity $emailAddress -SubjectKeywords $subjectKeywords -DeleteContent -StartDate $startDate -EndDate $endDate -Confirm:$false -PSTFolderPath "$pstFolder\$emailAddress.pst" -IncludeFolder "\Inbox" -ReportFile "$reportsFolder\$emailAddress.xml"  -BadItemLimit 10000
}

Unload-ExchangeSnapin # Remove this line if you want the Exchange Snapins to remain loaded at the conclusion of the script process

Tuesday, February 15, 2011

Search for Large Distribution Lists without Sending Restrictions

If you've worked in a large organization, you might have encountered the misdirected e-mail which finds itself being delivered to an unsecured distribution list. A well meaning employee uses the Global Address List, fat fingers the selection of the recipient and sends an e-mail not suited for a large audience. Even worse, a malicious departing employee takes advantage of the lack of security to say their not-so-fond farewell. This results storm of employees hitting reply to all (why can't Outlook have an 'Are you sure?' prompt for this button?) and writing "take me off this distribution list", "me too" and my personal favorite, "quit hitting reply to all" (usually in all caps, the font bolded and with multiple exclamation points). There are many reasons that this sort of event happens. No defined policy for the creation of distribution lists and mail-enabled security groups and the sending restrictions when the member list exceeds a certain size, human error in creation, organic growth of a small list over the years or the nuisance of nesting of groups (I'll tackle this problem later).

In the code sample below, I have a procedure to search the entire forest of the security context executing the script and return the group objects that are mail-enabled and do not have the authOrig and dLMemSubmitPerms attributes set. These two attributes are used for restricting who can send to a distribution list/mail-enabled security group within a forest. The authOrig attribute is for the storage of user object distinguished names and dLMemSubmitPerms is for the distinguished names of group objects. The code only looks at the first level of the members of the distribution list/mail-enabled security group. It does not take account the membership of a nested group. As I mentioned earlier, I will tackle that in a later post. The script will generate a report of the groups that have 25 members or more and are open to anyone to send. You can increase or decrease that value by modifying the threshold constant variable. I have also included and resolved the managedBy attribute to assist in identifying an employee who may have an understanding who should have the right to send.

Another attribute of interest and future blog post is msExchRequiredAuthToSendTo. This attribute controls the ability of external senders to deliver messages to distribution lists. If you have a good reason to have large, unsecured mail-enabled groups, you might want to restrict them from Internet based mailers -- especially spammers. This attribute is one method to protect those lists.
Function Get-LocalDomainController($objectDomain) {
 return ([System.DirectoryServices.ActiveDirectory.ActiveDirectorySite]::GetComputerSite()).Servers | Where-Object { $_.Domain.Name -eq $objectDomain } | ForEach-Object { $_.Name } | Select-Object -first 1
}
  
Function Get-ObjectADDomain($distinguishedName) {
 return ((($distinguishedName -replace "(.*?)DC=(.*)",'$2') -replace "DC=","") -replace ",",".")
}
  
Function Get-ActiveDirectoryObject($distinguishedName) {
 return [ADSI]("LDAP://" + (Get-LocalDomainController (Get-ObjectADDomain $distinguishedName)) + "/" + ($distinguishedName -replace "/","\/"))
}
#--------------------------------------------------------------------------------------------------#
Set-Variable -name forestRootDn -option Constant -value ([ADSI]("LDAP://" + (([System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()).name) + "/rootDSE")).defaultNamingContext
Set-Variable -name threshold -option Constant -value 25
#--------------------------------------------------------------------------------------------------#
$groupsInNonCompliance = @()
$objectConnection = New-Object -comObject "ADODB.Connection"
$objectCommand = New-Object -comObject "ADODB.Command"
$objectConnection.Open("Provider=ADsDSOObject;")
$objectCommand.ActiveConnection = $objectConnection

$ldapBase = "GC://$forestRootDn"
$ldapAttr = "distinguishedName"
$ldapScope = "subtree"

$ldapFilter = "(&(objectClass=group)(objectCategory=group)(!dLMemSubmitPerms=*)(!authOrig=*)(mail=*))"
$ldapQuery= "<$ldapBase>;$ldapFilter;$ldapAttr;$ldapScope"
$objectCommand.CommandText = $ldapQuery

$objectRecordSet = $objectCommand.Execute()
if(!$objectRecordSet.EOF) {
 while(!$objectRecordSet.EOF) {
  $groupObject = Get-ActiveDirectoryObject $objectRecordSet.Fields.Item('distinguishedName').Value
  if($groupObject.member.Count -ge $threshold) {
   $groupInformation = New-Object -typeName PSObject
   Add-Member -inputObject $groupInformation -type NoteProperty -name "domain" -value ((Get-ObjectADDomain $groupObject.distinguishedName).Split(".")[0]).ToUpper()
   Add-Member -inputObject $groupInformation -type NoteProperty -name "sAMAccountName" -value ($groupObject.sAMAccountName).ToString()
   Add-Member -inputObject $groupInformation -type NoteProperty -name "displayName" -value ($groupObject.displayName).ToString()
   Add-Member -inputObject $groupInformation -type NoteProperty -name "eMail" -value ($groupObject.mail).ToString()
   if($groupObject.managedBy) {
    $managedByObject = Get-ActiveDirectoryObject $groupObject.managedBy
    Add-Member -inputObject $groupInformation -type NoteProperty -name "managedBy" -value (((Get-ObjectADDomain $managedByObject.distinguishedName).Split(".")[0]).ToUpper() + "\" + ($managedByObject.sAMAccountName).ToString())
   } else {
    Add-Member -inputObject $groupInformation -type NoteProperty -name "managedBy" -value "N/A"
   }
   Add-Member -inputObject $groupInformation -type NoteProperty -name "memberCount" -value ($groupObject.member.count).ToString()
   $groupsInNonCompliance += $groupInformation
  }
  $objectRecordSet.MoveNext()
 }
} 

$groupsInNonCompliance | Export-Csv -path "Groups In Non-Compliance.csv" -noTypeInformation

Friday, February 11, 2011

Audit Tivoli Storage Manager Backup Client Schedules on Windows Servers

As any System Administrator knows you need to have a good backup. IBM Tivoli Storage Manager is a common platform used in large environments. It's a client-server platform with an installed client that once registered with the TSM server, backups the local server (client) to the destination backup storage. Keeping track of client versions, client nodes and log files can be burdensome if you have multiple TSM servers in different data centers. To keep abreast of this information, auditing is key. Knowing how the configuration of Tivoli Storage Manager Backup Client is stored for schedules gives us the ability to audit them. Most of the information resides in the registry under "HKEY_LOCAL_MACHINE\SOFTWARE\IBM". In the code example below, a list of server fully qualified domain names are stored in a text file and read into the script. To obtain this information using PowerShell, I am using the Microsoft.Win32.RegistryKey class to navigate the branches of the registry containing the configuration values of interest to me. One of those values, the option file path, allows me to obtain even more information about the backup -- the TSM server and port used for client-server communication. I take the local path of the option file and translate it to a UNC Path that I can remotely access, load the file into a foreach loop, attempt to located those two values and store them into strings. Just in case this process fails, I pre-populate the two strings with "FAILED". There are a few reasons that this could fail, the security context that is trying to access the option file doesn't have rights, the file doesn't exist (an uninstall failed to clear the registry) or the variables in the opt file we are searching for are not present.

This script does not report the servers lacking a client schedule. It is really important to know this information. Without a backup, you do not have recovery! I have another method that ensures that all servers that require backup are monitored for that process and if it was successful. If you do not, I would take some time to alter the script record the servers without schedules.

If you are running this script in an environment with both 32 bit and 64 bit Windows Server versions, you need to execute it from a 64 bit version of Windows Server. If you run it from a 32 bit and query the registry of a 64 bit server, the returned registry will be the 32 bit compatibility hive. TSM Backup Client has a native 64 bit version.
param([string]$file)

Function Get-TSMInfo($server, $tsmInfo) {
 $key = "SOFTWARE"
 $hKey = [Microsoft.Win32.RegistryHive]::LocalMachine
 $baseKey = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey($hKey, $server)
 foreach($rootKeyValue in ($baseKey.OpenSubKey($key)).GetSubKeyNames()) {
  if($rootKeyValue -eq "IBM" -and ($baseKey.OpenSubKey("$key\IBM\ADSM\CurrentVersion")).SubKeyCount -gt 2) {
   $tsmVersion = ($baseKey.OpenSubKey("$key\IBM\ADSM\CurrentVersion\BackupClient")).GetValue("PtfLevel")
   $tsmPath = ($baseKey.OpenSubKey("$key\IBM\ADSM\CurrentVersion\BackupClient")).GetValue("Path")
   $key = "SYSTEM\CurrentControlSet\Services"
   if($tsmVersion -ne "" -and $tsmPath -ne "") {
    foreach($keyValue in ($baseKey.OpenSubKey($key)).GetSubKeyNames()) {
     foreach($subKeyValue in ($baseKey.OpenSubKey("$key\$keyValue")).GetSubKeyNames()) {
      $clientNodeName = ""
      $errorLog = ""
      $optionsFile = ""
      $scheduleLog = ""
      if(($baseKey.OpenSubKey("$key\$keyValue").GetValue("Start")) -eq "2") {
       if($subKeyValue -eq "Parameters") {
        foreach($value in ($baseKey.OpenSubKey("$key\$keyValue\Parameters")).GetValueNames()) {
         if($value -eq "clientNodeName") {
          $clientNodeName = ($baseKey.OpenSubKey("$key\$keyValue\Parameters")).GetValue($value)
         } elseif($value -eq "errorLog") {
          $errorLog = ($baseKey.OpenSubKey("$key\$keyValue\Parameters")).GetValue($value)
         } elseif($value -eq "optionsFile") {
          $optionsFile = ($baseKey.OpenSubKey("$key\$keyValue\Parameters")).GetValue($value)
         } elseif($value -eq "scheduleLog") {
          $scheduleLog = ($baseKey.OpenSubKey("$key\$keyValue\Parameters")).GetValue($value)
         }
        }
       }
      }
      if($clientNodeName -ne "" -and $errorLog -ne "" -and $optionsFile -ne "" -and $scheduleLog -ne "") {
       $optionsFileUncPath = ("\\$server\" + ($optionsFile.SubString(0,1) + "$" + $optionsFile.SubString(2)))
       $tsmServer = "FAILED"
       $tsmClientPort = "FAILED"
       if(Test-Path -path $optionsFileUncPath) {
        foreach($line in (Get-Content -path $optionsFileUncPath)){
         if($line -match "TCPSERVERADDRESS") {
          $tsmServer = ($line -replace "TCPSERVERADDRESS","").Trim()
         }
         if($line -match "TCPCLIENTPORT") {
          $tsmClientPort = ($line -replace "TCPCLIENTPORT","").Trim()
         }
        }
       }
       $clientNodeInformation = New-Object -typeName PSObject
       Add-Member -inputObject $clientNodeInformation -type NoteProperty -name "server" -value $server
       Add-Member -inputObject $clientNodeInformation -type NoteProperty -name "tsmVersion" -value $tsmVersion
       Add-Member -inputObject $clientNodeInformation -type NoteProperty -name "installPath" -value $tsmPath
       Add-Member -inputObject $clientNodeInformation -type NoteProperty -name "tsmServer" -value $tsmServer
       Add-Member -inputObject $clientNodeInformation -type NoteProperty -name "tsmClientPort" -value $tsmClientPort
       Add-Member -inputObject $clientNodeInformation -type NoteProperty -name "scheduleName" -value $keyValue
       Add-Member -inputObject $clientNodeInformation -type NoteProperty -name "clientNodeName" -value $clientNodeName
       Add-Member -inputObject $clientNodeInformation -type NoteProperty -name "optionsFile" -value $optionsFile
       Add-Member -inputObject $clientNodeInformation -type NoteProperty -name "scheduleLog" -value $scheduleLog
       Add-Member -inputObject $clientNodeInformation -type NoteProperty -name "errorLog" -value $errorLog
       $tsmInfo += $clientNodeInformation
      }
     }
    }
   }
  }
 }
 return $tsmInfo
}

#--------------------------------------------------------------------------------------------------#

if(![bool]$file) {
 Write-Host "You are missing the `"-file`" command line argument" -foregroundColor Red
 Write-Host ""
 Write-Host "This is the file that contains the list of server fully qualified"
 Write-Host "domain names that you want to audit their TSM Clients"
 Write-Host ""
 while((![bool]$file)) {
  $file = Read-Host -prompt "`tFile"
 }
 Write-Host ""
}
if(!(Test-Path -path $file)) {
 Write-Host "Unable to locate: $file" -foregroundColor Red
 Write-Host ""
 Write-Host "Please review the correct path"
 Write-Host ""
 while(!(Test-Path -path $file)) {
  $file = Read-Host -prompt "`tFile"
 }
 Write-Host ""
}
 
$answer = $null
while($answer -ne "Y" -and $answer -ne "N") {
 $answer = Read-Host -prompt "Are you sure you want to use $file (Y/N)?"
}
 
if($answer -eq "N") {
 Write-Host ""
 Write-Host "Quiting..." -foregroundColor Yellow
 exit
}

$tsmInfo = @()

foreach($server in (Get-Content -path $file)) {
 Write-Host "Auditing $server"
 $tsmInfo = @(Get-TSMInfo $server $tsmInfo)
 Write-Host ("Schedules found: " + $tsmInfo.count)
}

$tsmInfo | Export-Csv -path "TSM Client Audit.csv" -noTypeInformation

Thursday, February 10, 2011

File System Rights -- Part 2

In this code example, I expand upon the NTFS rights exercise in the my blog post, "File System Rights -- Part 1". We use the same bitwise and operation to determine rights. This version, however, provides the same information found in the advance tab of Security for the "allow". I will demonstrate in later posts how to determine if a "deny" is selected. This script returns a much better report to provide to the non-technical user than the information recorded in Part 1.

Now that you have this information regarding NTFS permissions for a file system object, you can do some interesting things. You can monitor for changes. Alter permissions based on logic derived from the results returned. Clone permissions between objects. Consolidate permissions in defined access groups to reduce access bloat and speed up access -- I find this on non-Windows based NAS devices all the time. All of these topics I will provide examples in future blog posts.
Function Get-AdvancedPermissions($fileSystemRights) {
 $permissions = @()
 
 if ($fileSystemRights -band 0x1 -and $fileSystemRights -band 0x1 -and $fileSystemRights -band 0x2 -and $fileSystemRights -band 0x2 -and $fileSystemRights -band 0x4 -and $fileSystemRights -band 0x4 -and $fileSystemRights -band 0x8 -and $fileSystemRights -band 0x10 -and $fileSystemRights -band 0x20 -and $fileSystemRights -band 0x20 -and $fileSystemRights -band 0x40 -and $fileSystemRights -band 0x80 -and $fileSystemRights -band 0x100 -and $fileSystemRights -band 0x116 -and $fileSystemRights -band 0x10000 -and $fileSystemRights -band 0x20000 -and $fileSystemRights -band 0x20089 -and $fileSystemRights -band 0x200a9 -and $fileSystemRights -band 0x301bf -and $fileSystemRights -band 0x40000 -and $fileSystemRights -band 0x80000 -and $fileSystemRights -band 0x100000 -and $fileSystemRights -band 0x1f01ff) {
  $permissions += "Full Control"
 } 
 
 if ($fileSystemRights -band 0x1 -and  $fileSystemRights -band 0x10 -and  $fileSystemRights -band 0x100 -and  $fileSystemRights -band 0x10000 -and  $fileSystemRights -band 0x100000 -and  $fileSystemRights -band 0x116 -and  $fileSystemRights -band 0x2 -and  $fileSystemRights -band 0x20 -and  $fileSystemRights -band 0x20000 -and  $fileSystemRights -band 0x20089 -and  $fileSystemRights -band 0x200a9 -and  $fileSystemRights -band 0x301bf -and  $fileSystemRights -band 0x4 -and  $fileSystemRights -band 0x8 -and  $fileSystemRights -band 0x80) {
  $permissions += "Modify"
 }

 if ($fileSystemRights -band 0x200a9 -and $fileSystemRights -band 0x20 -and $fileSystemRights -band 0x1 -and $fileSystemRights -band 0x80 -and $fileSystemRights -band 0x8 -and $fileSystemRights -band 0x20000) { # -and $fileSystemRights -band 0x1 -and $fileSystemRights -band 0x80 -and $fileSystemRights -band 0x80x8 -and $fileSystemRights -band 0x20000) {
  $permissions += "Read & Execute"
 }

 if ($fileSystemRights -band 0x1 -and $fileSystemRights -band 0x1 -and $fileSystemRights -band 0x8 -and $fileSystemRights -band 0x20 -and $fileSystemRights -band 0x20000) {
  $permissions += "List Folder Contents"
 }

 if ($fileSystemRights -band 0x20089) { $permissions += "Read" }
 if ($fileSystemRights -band 0x116) { $permissions += "Write" }
 if ($fileSystemRights -band 0x20) { $permissions += "Traverse Folder / Execute File" }
 if ($fileSystemRights -band 0x1) { $permissions += "List Folder / Read Data" }
 if ($fileSystemRights -band 0x80) { $permissions += "Read Attributes" }
 if ($fileSystemRights -band 0x8) { $permissions += "Read Extended Attriibutes" }
 if ($fileSystemRights -band 0x2) { $permissions += "Create Files / Write Data" }
 if ($fileSystemRights -band 0x4) { $permissions += "Create Folders / Append Data" }
 if ($fileSystemRights -band 0x100) { $permissions += "Write Attributes" }
 if ($fileSystemRights -band 0x10) { $permissions += "Write Extended Attributes" }
 if ($fileSystemRights -band 0x40) { $permissions += "Delete Subfolders and Files" }
 if ($fileSystemRights -band 0x10000) { $permissions += "Delete" }  
 if ($fileSystemRights -band 0x20000) { $permissions += "Read Permissions" }
 if ($fileSystemRights -band 0x40000) { $permissions += "Change Permissions" }
 if ($fileSystemRights -band 0x80000) { $permissions += "Take Ownership" }

 if($fileSystemRights -band 0x10000000) {
  $permissions += "Full Control"
  $permissions += "Modify"
  $permissions += "Read & Execute"
  $permissions += "List Folder Contents"
  $permissions += "Write"
  $permissions += "Traverse Folder / Execute File"
  $permissions += "List Folder / Read Data"
  $permissions += "Read Attributes"
  $permissions += "Read Extended Attriibutes" 
  $permissions += "Create Files / Write Data"
  $permissions += "Create Folders / Append Data"
  $permissions += "Write Attributes"
  $permissions += "Write Extended Attributes"
  $permissions += "Delete Subfolders and Files"
  $permissions += "Delete"
  $permissions += "Read Permissions"
  $permissions += "Change Permissions"
  $permissions += "Take Ownership"
 }

 return $permissions
}

$uncPath = "\\server.ad.mycompany.local\share\directory"
#$uncPath = "\\server.ad.mycompany.local\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-AdvancedPermissions $ace.FileSystemRights
  foreach($permission in $permissions) {
   Write-Host "`t$permission"
  }
  Write-Host ("-" * 70)
 }
}

Wednesday, February 9, 2011

Exchange Accepted Mail Domains and User Primary SMTP Addresses

In large organizations, Exchange environments can handle numerous mail enabled domains. It is not uncommon to configure Exchange to accept mail for hundreds of domains. Keeping abreast of what domains are enabled and which users have them as their primary SMTP address is important. Companies launch products & services only to abandon them later. You might want to make a business decision to purge them from your mail system and no longer pay to own a domain. In the code sample below, I accomplish a few things. First, I discover which domains are configured in Exchange for the acceptance of e-mail by searching the Global Catalog for objects of the msExchAcceptedDomain class. Once those objects are discovered, I load those objects and obtain the domain through the msExchAcceptedDomainName attribute, sort them alphabetically and save them to a text file. Since I stored the enabled domains in an array, I can loop through them in a foreach loop and use them to discover users that have them as their primary SMTP address -- the goal I had when I wrote this script. I create two comma separated value text files from the results. The first is an audit of the domains and the number of users that have them as their primary SMTP. The second is an detailed export of those users.

Be aware that looking at just the users does not give you a complete picture of what objects in the forest are using mail enabled domains. Distribution Lists and Mail Enabled Security Groups can also accept mail from the Internet redirecting the message to the population of the members attribute and might be of interest in a review. To add this information to your query, change the LDAP filter to include them.
$ldapFilter = "(&(|(objectClass=user)(objectClass=group))(mail=*@$acceptedMailDomain))"
Another query that might be of interest is to expand the search beyond the primary SMTP address of the user object to include all proxy addresses associated to the user account. To accomplish this, change the LDAP filter to swap the mail attribute for the proxyAddresses multi-valued string attribute and include proxyAddresses in the returned attributes.
$ldapFilter = "(&(objectClass=user)(proxyAddresses=smtp:*@$acceptedMailDomain)(homeMDB=*))"
$ldapAttr = "distinguishedName,sAMAccountName,givenName,sn,proxyAddresses,telephoneNumber,streetAddress,l,st,postalCode,c"
You will also want to represent all matching e-mail addresses for that mail domain in your returned data file.
Add-Member -inputObject $mailDomainUser -type NoteProperty -name "eMail" -value [System.String]::join(";", ((($objectRecordSet.Fields.Item('proxyaddresses').Value) | Where-Object { $_ -match "smtp:(.*)@$acceptedMailDomain" }) | ForEach-Object { $_ -replace "smtp:","" }))
If your environment does handle a large number of accepted mail domains, expect this script to take some time to complete. I have included some Write-Host statements in the code to provide assurance that it is operating.
Function Get-LocalDomainController($objectDomain) {
 return ([System.DirectoryServices.ActiveDirectory.ActiveDirectorySite]::GetComputerSite()).Servers | Where-Object { $_.Domain.Name -eq $objectDomain } | ForEach-Object { $_.Name } | Select-Object -first 1
}
 
Function Get-ObjectADDomain($distinguishedName) {
 return ((($distinguishedName -replace "(.*?)DC=(.*)",'$2') -replace "DC=","") -replace ",",".")
}
 
Function Get-ActiveDirectoryObject($distinguishedName) {
 return [ADSI]("LDAP://" + (Get-LocalDomainController (Get-ObjectADDomain $distinguishedName)) + "/" + ($distinguishedName -replace "/","\/"))
}
#--------------------------------------------------------------------------------------------------#
Set-Variable -name forestRootDn -option Constant -value ([ADSI]("LDAP://" + (([System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()).name) + "/rootDSE")).defaultNamingContext
#--------------------------------------------------------------------------------------------------#
$acceptedMailDomains = @()
$mailDomainUsers = @()
$mailDomainsInfo = @()

$objectConnection = New-Object -comObject "ADODB.Connection"
$objectCommand = New-Object -comObject "ADODB.Command"
$objectConnection.Open("Provider=ADsDSOObject;")
$objectCommand.ActiveConnection = $objectConnection

$ldapBase = "GC://$forestRootDn"
$ldapFilter = "(objectClass=msExchAcceptedDomain)"
$ldapAttr = "distinguishedName"
$ldapScope = "subtree"
$ldapQuery = "<$ldapBase>;$ldapFilter;$ldapAttr;$ldapScope"

Write-Host "Exchange Mail Domains: " -noNewLine

$objectCommand.CommandText = $ldapQuery
$objectRecordSet = $objectCommand.Execute()

Write-Host $objectRecordSet.RecordCount

while(!$objectRecordSet.EOF) {
 $acceptedDomainObject = Get-ActiveDirectoryObject $objectRecordSet.Fields.Item('distinguishedName').Value
 $acceptedMailDomains += (($acceptedDomainObject.msExchAcceptedDomainName).ToString()).ToLower()
 $objectRecordSet.MoveNext()
}

$acceptedMailDomains = $acceptedMailDomains | Sort-Object

Set-Content -path "acceptedMailDomains.txt" -value ($acceptedMailDomains | Out-String)

foreach($acceptedMailDomain in $acceptedMailDomains) {
 Write-Host $acceptedMailDomain -noNewLine
 Write-Host ": " -noNewLine 

 $ldapFilter = "(&(objectClass=user)(mail=*@$acceptedMailDomain)(homeMDB=*))"
 $ldapAttr = "distinguishedName,sAMAccountName,givenName,sn,mail,telephoneNumber,streetAddress,l,st,postalCode,c"
 $ldapQuery = "<$ldapBase>;$ldapFilter;$ldapAttr;$ldapScope"
 $objectCommand.CommandText = $ldapQuery
 $objectRecordSet = $objectCommand.Execute()
 $userCount = $objectRecordSet.RecordCount
 
 Write-Host $userCount
 
 $mailDomainInfo = New-Object -typeName PSObject
 Add-Member -inputObject $mailDomainInfo -type NoteProperty -name "mailDomain" -value $acceptedMailDomain
 Add-Member -inputObject $mailDomainInfo -type NoteProperty -name "count" -value $userCount
 $mailDomainsInfo += $mailDomainInfo
 
 while(!$objectRecordSet.EOF) {
  $userObjectDomain = Get-ObjectADDomain $objectRecordSet.Fields.Item('distinguishedName').Value

  $mailDomainUser = New-Object -typeName PSObject
  Add-Member -inputObject $mailDomainUser -type NoteProperty -name "mailDomain" -value $acceptedMailDomain
  Add-Member -inputObject $mailDomainUser -type NoteProperty -name "domain" -value $userObjectDomain.Split(".")[0]
  Add-Member -inputObject $mailDomainUser -type NoteProperty -name "sAMAccountName" -value $objectRecordSet.Fields.Item('sAMAccountName').Value
  Add-Member -inputObject $mailDomainUser -type NoteProperty -name "firstName" -value $objectRecordSet.Fields.Item('givenName').Value
  Add-Member -inputObject $mailDomainUser -type NoteProperty -name "lastName" -value $objectRecordSet.Fields.Item('sn').Value
  Add-Member -inputObject $mailDomainUser -type NoteProperty -name "eMail" -value $objectRecordSet.Fields.Item('mail').Value
  Add-Member -inputObject $mailDomainUser -type NoteProperty -name "telephoneNumber" -value $objectRecordSet.Fields.Item('telephoneNumber').Value
  Add-Member -inputObject $mailDomainUser -type NoteProperty -name "streetAddress" -value ($objectRecordSet.Fields.Item('streetAddress').Value -replace "`r`n",", ")
  Add-Member -inputObject $mailDomainUser -type NoteProperty -name "city" -value $objectRecordSet.Fields.Item('l').Value
  Add-Member -inputObject $mailDomainUser -type NoteProperty -name "state" -value $objectRecordSet.Fields.Item('st').Value
  Add-Member -inputObject $mailDomainUser -type NoteProperty -name "zipCode" -value $objectRecordSet.Fields.Item('postalCode').Value
  Add-Member -inputObject $mailDomainUser -type NoteProperty -name "country" -value $objectRecordSet.Fields.Item('c').Value
  $mailDomainUsers += $mailDomainUser

  $objectRecordSet.MoveNext() 
 }
}

$mailDomainUsers | Export-Csv -path "mailDomainUsers.csv" -noTypeInformation
$mailDomainsInfo | Export-Csv -path "mailDomainInformation.csv" -noTypeInformation

Tuesday, February 8, 2011

Active Directory Forest Domain Controller Report using .Net

One of the nice features of PowerShell is the ability to leverage .Net. In the code sample below, I am able to export into a comma separated values text file a listing of all domain controllers in an Active Directory forest with some key information. Nowhere in the code am I specifying information about the root of the forest. Using the System.DirectoryServices.ActiveDirectory.Forest class, I am able to begin with a rich starting point to gather the information to describe the domain controllers in the forest of the security context of the account I execute the script.
Set-Variable -name forestInformation -option Constant -value ([System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest())
Set-Variable -name globalCatalogs -option Constant -value ($forestInformation.GlobalCatalogs | ForEach-Object { $_.Name })

$domainControllers = @()

foreach($domain in $forestInformation.Domains) {
 $domainName = (($domain.Name).Split(".")[0]).ToUpper()
 foreach($domainController in $domain.domainControllers | Sort-Object -property SiteName, Name) {
  $roles = @()
  if($globalCatalogs -contains $domainController.Name) {
   $roles += "Global Catalog"
  }
  if($domainController.Name -eq $domain.PdcRoleOwner){
   $roles += "PDC Emulator"
  }
  if($domainController.Name -eq $domain.RidRoleOwner){
   $roles += "Relative ID Master"
  }
  if($domainController.Name -eq $domain.InfrastructureRoleOwner){
   $roles += "Infrastructure Master"
  }
  if($domain.name -eq $forestInformation.RootDomain.Name -and $domainController.Name -eq $forestInformation.SchemaRoleOwner) {
   $roles += "Schema Master"
  }
  if($domain.name -eq $forestInformation.RootDomain.Name -and $domainController.Name -eq $forestInformation.NamingRoleOwner) {
   $roles += "Domain Naming Master"
  }

  $server = New-Object -typeName PSObject
  Add-Member -inputObject $server -type NoteProperty -name "domain" -value $domainName
  Add-Member -inputObject $server -type NoteProperty -name "fqdn" -value ($domainController.Name).ToLower()
  Add-Member -inputObject $server -type NoteProperty -name "ipAddress" -value $domainController.IPAddress
  Add-Member -inputObject $server -type NoteProperty -name "osVersion" -value $domainController.OSVersion
  Add-Member -inputObject $server -type NoteProperty -name "siteName" -value $domainController.SiteName
  Add-Member -inputObject $server -type NoteProperty -name "roles" -value ([System.String]::join(", ", $roles))
  $domainControllers += $server
 }
}

$domainControllers | Export-Csv -path ($forestInformation.RootDomain.Name + " Domain Controllers.csv") -noTypeInformation

Friday, February 4, 2011

Get the Fully Qualified Domain Name of a User's Exchange Server using the homeMDB Attribute

When you extend the schema to install Exchange (2003 & 2007), one of the attributes Microsoft doesn't add to the user object is an attribute to store the fully qualified domain name of Exchange server hosting the mailbox. That is really useful information! There are attributes that give you pretty good hints if you are aware of the environment such as homeMDB, homeMTA and msExchHomeServerName. The best you can glean from these attributes is the host name of the server. In a small forest, that might be good enough. In a large, global, multi-domain forest, that is not so great. Using homeMDB, we can determine the Mailbox Store of the user and obtain more information about it as an msExchMDB class object. From there, we look up the parent of this object, a Storage Group (msExchStorageGroup class object). At this point, we need to do another parental lookup, an Information Store (msExchInformationStore class object). And finally, if we look up the parental object of the information store, we discover the Exchange Server (msExchExchangeServer class object). Certainly, Microsoft would provide a dNSHostName attribute on the msExchExchangeServer object class. Right? That would make so much sense! WRONG! We are not beaten yet. There is an multi-valued attribute called networkAddress which stores information using the Microsoft Interface Definition Language. In the fifth element (remember the elements start at zero), we find ncacn_ip_tcp which in MIDL specifies the fully qualified domain name for the Exchange server. Finally! We are not done yet, however, we need to remove the "ncacn_ip_tcp" from the fifth element and to be fancy, return the resultant string in lower case.

In the example code below, I accomplish this minor feat of discovering the FQDN using the great-grandparent of the object whose distinguished name is stored in the homeMDB attribute of a user and use up a daily supply of parenthesizes in one line. I have tested this in an Exchange 2003 and Exchange 2007 environment. I have not in an Exchange 2010 environment. I am starting that project soon so I will update this blog post in the future or if you have such an environment handy, leave a comment on the success or failure of this code snippet.

I use techniques from my "Get Object Active Directory Domain from distinguishedName" and "Return a Local Domain Controller in the Current Site for a Specific Domain" blog entries and rolled them into functions. As shown in Enumerate Objects in an Organizational Unit, I use those two functions to easily return objects from Active Directory from a third function that relies on the other two.
Function Get-LocalDomainController($objectDomain) {
 return ([System.DirectoryServices.ActiveDirectory.ActiveDirectorySite]::GetComputerSite()).Servers | Where-Object { $_.Domain.Name -eq $objectDomain } | ForEach-Object { $_.Name } | Select-Object -first 1
}

Function Get-ObjectADDomain($distinguishedName) {
 return ((($distinguishedName -replace "(.*?)DC=(.*)",'$2') -replace "DC=","") -replace ",",".")
}

Function Get-ActiveDirectoryObject($distinguishedName) {
 return [ADSI]("LDAP://" + (Get-LocalDomainController (Get-ObjectADDomain $distinguishedName)) + "/" + ($distinguishedName -replace "/","\/"))
}
#--------------------------------------------------------------------------------------------------#
$userDn = "CN=Doe\, John,CN=Users,DC=ad,DC=mydomain,DC=Local"
$userObject = Get-ActiveDirectoryObject $userDn
$mailServer = ((((((Get-ActiveDirectoryObject $userObject.homeMDB).psbase.parent).psbase.parent).psbase.parent).networkAddress[4]).ToString() -replace "ncacn_ip_tcp:","").ToLower()
Write-Host $mailServer

Wednesday, February 2, 2011

Enumerate Objects in an Organizational Unit

In the example code below, I show how to return the objects within an organizational unit without venturing into subordinate organizational units. A future post will cover how to enumerate objects in nested organizational units which is similar to walking a directory tree as I did in "Searching for Files in Nested Directories". To make the exercise a little interesting, I am pointing to the Users container in the root of a forest of the security context the script is executed, building a report of user objects and saving selected attributes in a comma separated value text file. I use techniques from my "Get Object Active Directory Domain from distinguishedName" and "Return a Local Domain Controller in the Current Site for a Specific Domain" blog entries and rolled them into functions. Those two functions are used by a new function called Get-ActiveDirectoryObject which assists in obtaining the organizational unit object and the user object's manager attribute's user object. One note, pay attention to string replace I do on the distinguished name variable in the Get-ActiveDirectoryObject function. A common gotcha I have encountered are forward slashes (i.e., "/") in the distinguished name. You have to escape them in order to obtain the object or you get see red error messages.
Function Get-LocalDomainController($objectDomain) {
 return ([System.DirectoryServices.ActiveDirectory.ActiveDirectorySite]::GetComputerSite()).Servers | Where-Object { $_.Domain.Name -eq $objectDomain } | ForEach-Object { $_.Name } | Select-Object -first 1
}

Function Get-ObjectADDomain($distinguishedName) {
 return ((($distinguishedName -replace "(.*?)DC=(.*)",'$2') -replace "DC=","") -replace ",",".")
}

Function Get-ActiveDirectoryObject($distinguishedName) {
 return [ADSI]("LDAP://" + (Get-LocalDomainController (Get-ObjectADDomain $distinguishedName)) + "/" + ($distinguishedName -replace "/","\/"))
}

#--------------------------------------------------------------------------------------------------#
# Modify the startingOU variable below to a significant organizational unit that contains user objects
Set-Variable -name forestRootDn -option Constant -value ([ADSI]("LDAP://" + (([System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()).name) + "/rootDSE")).defaultNamingContext
Set-Variable -name startingOu -option Constant -value "CN=Users,$forestRootDn"
#--------------------------------------------------------------------------------------------------#

$ouObject = Get-ActiveDirectoryObject $startingOu
$users = @()
foreach($childObject in $ouObject.psBase.Children) {
 if($childObject.objectClass -eq "user") {
  if($childObject.manager) {
   $managerObject = Get-ActiveDirectoryObject ($childObject.manager).ToString()
   if($managerObject.displayName) {
    $manager = ($managerObject.displayName).ToString()
   } else {
    $manager = (($managerObject.sn).ToString() + ", " + ($managerObject.givenName).ToString())
   }
  } else {
   $manager = "N/A"
  }
  $user = New-Object -typeName PSObject
  Add-Member -inputObject $user -type NoteProperty -name "domain" -value (Get-ObjectADDomain ($childObject.distinguishedName).ToString()).Split(".")[0]
  Add-Member -inputObject $user -type NoteProperty -name "sAMAccountName" -value (($childObject.sAMAccountName).ToString()).ToLower()
  Add-Member -inputObject $user -type NoteProperty -name "givenName" -value ($childObject.givenName).ToString()
  Add-Member -inputObject $user -type NoteProperty -name "sn" -value ($childObject.sn).ToString()
  Add-Member -inputObject $user -type NoteProperty -name "displayName" -value ($childObject.displayName).ToString()
  Add-Member -inputObject $user -type NoteProperty -name "mail" -value (($childObject.mail).ToString()).ToLower()
  Add-Member -inputObject $user -type NoteProperty -name "title" -value ($childObject.title).ToString()
  Add-Member -inputObject $user -type NoteProperty -name "department" -value ($childObject.department).ToString()
  Add-Member -inputObject $user -type NoteProperty -name "manager" -value $manager
  Add-Member -inputObject $user -type NoteProperty -name "physicalDeliveryOfficeName" -value ($childObject.physicalDeliveryOfficeName).ToString()
  Add-Member -inputObject $user -type NoteProperty -name "streetAddress" -value (($childObject.streetAddress).ToString() -Replace "`r`n",", ")
  Add-Member -inputObject $user -type NoteProperty -name "l" -value ($childObject.l).ToString()
  Add-Member -inputObject $user -type NoteProperty -name "st" -value ($childObject.st).ToString()
  Add-Member -inputObject $user -type NoteProperty -name "postalCode" -value ($childObject.postalCode).ToString()
  Add-Member -inputObject $user -type NoteProperty -name "c" -value ($childObject.c).ToString()
  Add-Member -inputObject $user -type NoteProperty -name "telephoneNumber" -value ($childObject.telephoneNumber).ToString()
  $users += $user
 }
}
$users | Export-Csv -path (($ouObject.name).ToString() + ".csv") -noTypeInformation

Tuesday, February 1, 2011

Scheduling Tasks in PowerShell for Windows Server 2008 -- Part 1: Monthly Task

Windows Server 2008 updates one of my biggest complaints in Windows Server 2003 relating to scheduling automated tasks programatically. I have always appreciated the ease of doing this in Linux/UNIX via crontab. That ease was just not there in Windows Server 2003 as at is very clunky. However, I feel that creating a scheduled task in Windows Server 2003 manually is much easier and faster than Windows Server 2008 -- but that's me. In the example below, I create a monthly scheduled task that occurs at 6 am on the 15th of the month starting in February 2011. I am storing the task in a subfolder off the root in Scheduled Tasks that I manually created to segregate the task from other application registrations. The task will run under another security context than the one that created the task. I would prefer to capture the password securely for that context and supply it that way to the RegisterTaskDefinition but I have not discovered how to perform this with the provider. To provide a modicum of security, I clear the $password variable at the end of the script. To reduce security risks in your environment, always execute scripts with the least security privilege required to perform the function.

In future posts, I will provide further examples that show the different intervals that you can schedule tasks. This example is a basic task as there are many areas where you can fine tune the task.
# Parameters to modify
$taskName = "Export-InterestingInformation.ps1"
$taskWorkingDirectory = "C:\PowerShell"
$taskAuthor = "ad\myaccount"
$taskDescription = "The Monthly Interesting Information Report"
$taskSecurityPrincipal = "ad\reporting"
$taskSheduledTaskFolder = "\MyTasks"
$startTime = (Get-Date "02/15/2011 06:00:00" -Format s)

# Would like to use -asSecureString but RegisterTaskDefinition does not accept it
# Look over your shoulder before typing
$password = Read-Host -prompt "$taskSecurityPrincipal Password"

# The meaty parts

$taskService = New-Object -ComObject Schedule.Service
$taskService.Connect()

$rootFolder = $taskService.GetFolder($taskSheduledTaskFolder)

$taskDefinition = $taskService.NewTask(0)

$registrationInformation = $taskDefinition.RegistrationInfo
$registrationInformation.Description = $taskDescription
$registrationInformation.Author = $taskAuthor

$taskPrincipal = $taskDefinition.Principal
$taskPrincipal.LogonType = 1
$taskPrincipal.UserID = $taskSecurityPrincipal
$taskPrincipal.RunLevel = 0

$taskSettings = $taskDefinition.Settings
$taskSettings.StartWhenAvailable = $true
$taskSettings.RunOnlyIfNetworkAvailable = $true
$taskSettings.Priority = 7
$taskSettings.ExecutionTimeLimit = "PT2H"

$taskTriggers = $taskDefinition.Triggers

$executionTrigger = $taskTriggers.Create(4) 
$executionTrigger.DaysOfMonth = 16384 # http://msdn.microsoft.com/en-us/library/aa380735(v=vs.85).aspx
$executionTrigger.StartBoundary = $startTime

$taskAction = $taskDefinition.Actions.Create(0)
$taskAction.Path = "%SystemRoot%\system32\WindowsPowerShell\v1.0\powershell.exe"
$taskAction.Arguments = "-command `"$taskWorkingDirectory\$taskName`""
$taskAction.WorkingDirectory = $taskWorkingDirectory

# 6 == Task Create or Update
# 1 == Password must be supplied at registration
$rootFolder.RegisterTaskDefinition($taskName, $taskDefinition, 6, $taskSecurityPrincipal, $password, 1)

# Since we captured this in plain text I am going to nuke the value
# Not 100% security
Clear-Variable -name password