Browsing the archives for the PowerShell category.


From Scripts to Modules

PowerShell, Work Experience

When I wrote my post on the PowerShell Audit Reports, it caught the attention of Thomas Lee who decided to take them up and package them into a PowerShell v2 module.

The output of his work is shown on his blog and I think it provides a very nice example for getting from a script repository to a distributable module. I could definitely see the value of this as many of the scripts I use today I share with my coworkers. This would allow me to “package” together these scripts into releases to be distributed.

Good stuff.

1 Comment

PowerShelling Audit Reports

PowerShell, Work Experience

Part of working in corporate IT is the necessity to create and demonstrate compliance with general computing controls. In my environment part of those controls consist of creating exports of the membership of certain Active Directory groups. Since I am a fan of automation I put together some PowerShell scripts to speed up this task but it also comes in handy for those one-off “Who is in such-and-such group?” requests.

First, you’ll need two requirements.

  • PowerShell 2.0 CTP (I use the convertto-csv cmdlet that does not exist in PSv1. I suppose I could write a routine to do the conversion – but why when 2.0 is coming along nicely?)
  • Quest ActiveRoles AD Cmdlets (Once again, I could write the whole thing using System.DirectoryServices, but why when the heavy lifting has been done for you?)

With that out of the way, the routine consists of three different scripts. First, the workhorse of the routine, Get-RecursiveGroupMembership.ps1:

param (
    [string] $distinguishedname,
    [bool] $addOtherTypes = $false
    )
$members = @()

$this = (Get-QADGroup $distinguishedname).member | Get-QADObject
$this | foreach {
    if ($_.type -eq ‘user’) {
        $members += $_
        }
    elseif ($_.type -eq ‘group’) {
        Write-Host "Adding sub group $_"
        $members += .\Get-RecursiveGroupMembership.ps1 $_.dn $addOtherTypes
        }
    else {
        if ($addOtherTypes -eq $true) {
            $members += $_
            }
        else {
            Write-Host "Non user/group member detected. Not added. Use -addOtherTypes flag to add."
        }
    }
}
return $members

This script is very handy for a lot of things and gives me more power for recursive membership listing than the Get-QADGroupMember cmdlet alone. The other thing that the Get-QADGroupMember cmdlet falls short on is enumerating membership when the group is in another domain – a common occurrence in my environment – hence the use of QADObject and then reading the members attribute.

You will also see the switch. By default the script returns only user objects. You can throw the second parameter (the $addOtherTypes) to true to get all object types.

Future functionality for this script could include a parameter for specifying types to return.

On to the second script, Audit-QuickGroup.ps1, which allows me to pass the distinguishedName of a group in any domain and have the results of that group membership written to a csv file of the same name. This script relies on the first.

param ([string] $name)
$csvdata = .\Get-RecursiveGroupMembership.ps1 $name | select name,type,dn,title,office,description | convertto-csv -NoTypeInformation
$filename = $name + ".csv"
[String]$reportdate = "Report Generated: " + [datetime]::Now
$f = new-item -itemtype file $filename
add-content $f "Audit Report – Active Directory Group – $name"
add-content $f $reportdate
add-content $f $csvdata

This script is pretty simple, taking the output, selecting my most commonly requested attributes, and writing it to a csv file with some header information. Future functionality may include the ability to specify an output filename (optional) and including the header or not.

Lastly, for bulk operations, I needed a way to use the filtering abilities of the Quest Get-QADGroup cmdlet to do the export of multiple groups, written to multiple files with one command. Sure, I could have used a little “Get-QADGroup blah –filter moreblah | foreach { loop previous script }”, but why do that when I can do this in my Audit-MultipleGroups.ps1 script?

# take group input
param ([string] $GroupInput)

#get groups
$GroupList = get-qadgroup $groupinput

# iterate through groups, creating output
foreach ($Group in $GroupList) {
    Write-Host $group.dn
    $GroupMembers = .\Get-RecursiveGroupMembership.ps1 $group.DN | select name,type,dn,title,office,description | convertto-csv -NoTypeInformation
    #now create file
    $filename = $Group.Name + ".csv"
    [String]$reportdate = "Report Generated: " + [datetime]::Now
    $file = New-Item -ItemType file $filename -Force
    Add-Content $file "Audit Report – Active Directory Group Membership"
    Add-Content $file $reportDate
    Add-Content $file $groupMembers
    }

With the swipe of one command, like:

PS> .\Audit-MultopleGroups.ps1 SEC_ADM_*

Provided you have a good Active Directory group naming policy this can output all of the groups matching that prefix into their own csv files with one little command.

So there you go. A very useful base script, a more customer-focused output script, and a purpose-built script all reducing the amount of work necessary to report on group membership in Active Directory. There is one last scenario to deal with in my environment, local computer group membership. The script below will take the computer name and group name as parameters and output the members into a text file. I’ve called it Audit-LocalGroupMembership.ps1.

#local group member enumeration
param (
    [string] $Server,
    [string] $GroupName
)
$MemberNames = @()
$Group= [ADSI]"WinNT://$Server/$GroupName,group"
$Members = @($Group.psbase.Invoke("Members"))
$Members | ForEach-Object {$MemberNames += $_.GetType().InvokeMember("Name", ‘GetProperty’, $null, $_, $null)}

#file routine
$filename = $server + " " + $GroupName + ".txt"
$f = New-Item -ItemType file $filename
[string]$reportdate = "Date: " + (Get-Date).tostring(’yyyyMMdd’)
Add-Content $f "Local Group Membership Report"
Add-Content $f "Server: $server"
Add-Content $f "Group : $groupname"
Add-Content $f $reportdate
Add-Content $f $membernames
return $membernames

Happy New Year and happy PowerShelling!

3 Comments

PowerShell One Liner for Directory Lookup

PowerShell

Upon request from one of my coworkers, a one-liner for DN from an AD account name.

$a=read-host; $s=New-Object
directoryservices.directorysearcher(”LDAP://dc=yerdomainhere”);
$s.filter=”(sAMAccountName=$a)”; $s.findone().path

Yeah, sad use of the semicolon I know, but it’s a 2 minute job. :)

No Comments

Windows Server 2008 admin tools for Vista

Exchange, PowerShell, Vista, Windows Server

Now available and in the "must download" list of software.

Microsoft® Remote Server Administration Tools enables IT administrators to remotely manage roles and features in Windows Server® 2008 from a computer running Windows Vista® with Service Pack 1 (SP1).

Download Link (x86) | Download Link (x64)

While you are out, pick up the HyperV admin console for Vista SP1 as well.

No Comments

Today’s Fun with PowerShell

Exchange, PowerShell, Windows Server

On this episode of Fun with PowerShell, we dive into the realm of retrieving account information from various sources using the DirectoryEntry object. If you are familiar with the ADSI interfaces, this should give you a good idea of how to transfer knowledge from that medium to the PowerShell/.NET way of thinking.

All the code examples are based on work I’ve done today, but as always YMMV. There’s always a better, faster, more creative way to do coding, and I highly encourage that thinking.

So, what is a DirectoryEntry object? Simply an instance of that class, borrowed from the .NET framework. You can use a DirectoryEntry to access any of the ADSI service providers, so LDAP, IIS, NDS, and WinNT. I am going to touch on LDAP and WinNT for this article.

Let’s start with the basics. You have an Active Directory domain. You want to find all servers in that domain (those that have checked in, at least). First, you need a starting point for your search.

PS C:\> $ad = New-Object DirectoryServices.DirectoryEntry("LDAP://dc=company,dc=com")

Could I specify a particular domain controller? Sure!

PS C:\> $ad = New-Object DirectoryServices.DirectoryEntry("LDAP://domainctrl1.company.com:389")

What if we wanted to start further down the tree?

PS C:\> $ad = New-Object DirectoryServices.DirectoryEntry("LDAP://ou=something,ou=somewhere,dc=company,dc=com")

or

PS C:\> $ad = New-Object DirectoryServices.DirectoryEntry("LDAP://domainctrl1.company.com:389/ou=something,ou=somewhere,dc=company,dc=com")

Can we make it even shorter? If your workstation is a member of the domain you are looking for, then we can.

PS C:\> $ad = [ADSI]

This is very nifty. At the lowest level, we are just selecting a particular object in the directory. For the purposes of our search, this happens to be a root or OU level of the directory. You could pluck out a particular individual object and manipulate from there. Try it with your user account.

PS C:\> $user = New-Object DirectoryServices.DirectoryEntry("LDAP://cn=Dillon TenBrink,ou=geeks,dc=company,dc=com") | fl *

Now that we have our $ad variable, representing our search root, we need a searcher.

PS C:\> $searcher = New-Object DirectoryServices.DirectorySearcher($ad)

Once we have a searcher, we have a selection of settings to flip on it to execute our query.

PS C:\> $searcher

CacheResults             : True
ClientTimeout            : -00:00:01
PropertyNamesOnly        : False
Filter                   : (objectClass=*)
PageSize                 : 0
PropertiesToLoad         : {}
ReferralChasing          : External
SearchScope              : Subtree
ServerPageTimeLimit      : -00:00:01
ServerTimeLimit          : -00:00:01
SizeLimit                : 0
SearchRoot               : System.DirectoryServices.DirectoryEntry
Sort                     : System.DirectoryServices.SortOption
Asynchronous             : False
Tombstone                : False
AttributeScopeQuery      :
DerefAlias               : Never
SecurityMasks            : None
ExtendedDN               : None
DirectorySynchronization :
VirtualListView          :
Site                     :
Container                :

From here, we go to work defining our filter and the properties we would like to get back.

PS C:\> $searcher.Filter = "(&(objectClass=Computer)(operatingSystem=*Server*))"

PS C:\> $searcher.SizeLimit = 1000

PS C:\> $searcher.PropertiesToLoad.Add("cn")

PS C:\> $searcher.PropertiesToLoad.Add("operatingSystem")

You could define more options if you needed. Check the documentation on the DirectorySearcher class for more information.

From here you can execute the .FindOne() or .FindAll() methods to return the results of your query.

Let’s move on to a different animal using the DirectoryEntry class to show some versatility. This time we’ll use the WinNT ADSI interface.

The WinNT interface is still, in my opinion, the best way to get information on local accounts and groups remotely on a Windows box. Here we are going to use it to get a list of the local users on a server PDX01. First, like before, we create our DirectoryEntry.

PS C:\> $Sys = New-Object DirectoryServices.DirectoryEntry("WinNT://PDX01,computer")

If you performed a $Sys | fl * at this point, you can see all the properties of this object, just like your user account from AD above when we’re using the LDAP provider. What we’re after though, is the members, or "children", of this computer.

PS C:\> $Sys.children

Executing this command will return users, groups, and services. Since we’re just after the users, let’s filter it down.

PS C:\> $Sys.children | where {$_.SchemaClassName -match "User"}

Much better. Now we have our list of users. If we wanted to save this for later, dump it into a variable.

PS C:\> $Users = $Sys.children | where {$_.SchemaClassName -match "User"}

And then pipe all the details out to the console.

PS C:\> $Users | fl *

There you go.

From both of these starting blocks, you can then use your scripting prowess to harness the results of these queries to do your bidding. Take the AD server query, drop it into a loop with the local account query, and you have a quick and powerful local account audit script. Extend it a little more and take the local users, filter by their password age attribute and you have a functional password age checking script for local accounts. The possibilities are endless once you can retrieve the data sets.

Is this easier in vbScript or VB.NET?

My personal preference is to write these particular queries in VB.NET because I typically have more data analysis to do against other systems. Just my preference. That’s not to say I couldn’t write those interfaces and analysis in PowerShell; just that I haven’t found a need quite yet. The PowerShell way is very useful though, and quicker, when just plain, raw data is required and I use it frequently in those scenarios.

Benefits over vbScript? You can walk though this line by line, in real time, and adjust as you go to get your results exactly the way you want them. You wouldn’t want to write that script, execute, correct, and run again from the start over and over until you got what you wanted, right? PowerShell provides the advantage of having feedback line-by-line as you progress. Then, save your refined statements as a .ps1 file and reuse it later or call it in other scripts.

Happy scripting!

1 Comment