Get All Members of a Local Group Using PowerShell

I wrote a function a while back that is used to query a local group on a remote or local system (or systems) and based on the –Depth parameter, will perform a recursive query for all members of that group to include local and domain groups and users. I felt that it was something worth sharing out just in case someone has a need for it.  It also isn’t the same script that I wrote back in January here: https://learn-powershell.net/2013/01/22/find-and-report-members-of-a-local-group/

To avoid an issue with circular groups (probably not the technical name for it), I use a hash table to manage the local/domain groups to ensure that they are not queried again later on. By circular groups, I am talking about groups that are members of a parent group that may have the parent group listed as a member later on down the group membership. A more understandable example is here:

image

Because Administrators exist in Group3 and Administrators has Group3 as a member, this madness will never stop or will stop when it hits some limit on depth. Regardless, this would be a very annoying thing to have happen.

    [cmdletbinding()]
    Param (
        [parameter(ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$True)]
        [Alias('CN','__Server','Computer','IPAddress')]
        [string[]]$Computername = $env:COMPUTERNAME,
        [parameter()]
        [string]$Group = "Administrators",
        [parameter()]
        [int]$Depth = ([int]::MaxValue)
    )

Nothing really new here. I am setting up my parameters for Computername, a Group name (which defaults to’Administrators and the Depth limit. You will notice that I am using [int]::MaxValue which is 2147483647 which is my ”unlimited” recursion. If someone has a nested group that far down, then my hats off to you!

#region Extra Configurations
Write-Verbose ("Depth: {0}" -f $Depth)
#endregion Extra Configurations
#region Helper Functions
Function Get-NetBIOSDomain {
    Try {
        $Domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()
        $Root = $Domain.GetDirectoryEntry()
        $Base = ($Root.distinguishedName)

        # Use the NameTranslate object.
        $Script:Translate = New-Object -comObject "NameTranslate"
        $Script:objNT = $Translate.GetType()

        # Initialize NameTranslate by locating the Global Catalog.
        $objNT.InvokeMember("Init", "InvokeMethod", $Null, $Translate, (3, $Null))

        # Retrieve NetBIOS name of the current domain.
        $objNT.InvokeMember("Set", "InvokeMethod", $Null, $Translate, (1, "$Base"))
        $objNT.InvokeMember("Get", "InvokeMethod", $Null, $Translate, 3)  
    } Catch {Write-Warning ("{0}" -f $_.Exception.Message)} 
}

This piece of code starts out with some verbose output (if using –Verbose) that will show the parameters being used as well as what the current depth that will be allowed for the nested groups.

The Get-NetBIOSDomain function is used only to help get the distinguishedName of a domain group in the Get-DomainGroupMember function (shown later in this article).

Function Get-LocalGroupMember {
    [cmdletbinding()]
    Param (
        [parameter()]
        [System.DirectoryServices.DirectoryEntry]$LocalGroup
    )
    $Counter++
    # Invoke the Members method and convert to an array of member objects.
    $Members= @($LocalGroup.psbase.Invoke("Members"))

    ForEach ($Member In $Members) {                
        Try {
            $Name = $Member.GetType().InvokeMember("Name", 'GetProperty', $Null, $Member, $Null)
            $Path = $Member.GetType().InvokeMember("ADsPath", 'GetProperty', $Null, $Member, $Null)
            # Check if this member is a group.
            $isGroup = ($Member.GetType().InvokeMember("Class", 'GetProperty', $Null, $Member, $Null) -eq "group")
            If (($Path -like "*/$Computer/*")) {
                $Type = 'Local'
            } Else {$Type = 'Domain'}
            New-Object PSObject -Property @{
                Computername = $Computer
                Name = $Name
                Type = $Type
                ParentGroup = $LocalGroup.Name[0]
                isGroup = $isGroup
                Depth = $Counter
            }
            If ($isGroup) {
                Write-Verbose ("{0} is a group" -f $Name)
                # Check if this group is local or domain.
                Write-Verbose ("Checking if Counter: {0} is less than Depth: {1}" -f $Counter, $Depth)
                If ($Counter -lt $Depth) {
                    If ($Type -eq 'Local') {
                        If ($Groups[$Name] -notcontains 'Local') {
                            Write-Verbose ("{0}: Getting local group members" -f $Name)
                            $Groups[$Name] += ,'Local'
                            # Enumerate members of local group.
                            Get-LocalGroupMember $Member
                        }
                    } Else {
                        If ($Groups[$Name] -notcontains 'Domain') {
                            Write-Verbose ("{0}: Getting domain group members" -f $Name)
                            $Groups[$Name] += ,'Domain'
                            # Enumerate members of domain group.
                            Get-DomainGroupMember $Member $Name $True
                        }
                    }
                }
            }
        } Catch {
            Write-Warning ("{0}" -f $_.Exception.Message)
        }
    }
}

 

Get-LocalGroupMember is my first helper function when it comes to getting members of a group. In this case, I am gathering the members of a local group. A number of possibilities exist here such as whether the member is actually a User, or if it is another group that is either local on the system or a domain group. Based on the group type, it will either call itself again for the local group or call the Get-DomainGroupMember function which will be explained next. You can tell from my $Groups variable that it is the hash table used to make sure that the group hasn’t already queried as well as the counter which helps to determine the depth level of the current group and its members.

Function Get-DomainGroupMember {
    [cmdletbinding()]
    Param (
        [parameter()]
        $DomainGroup, 
        [parameter()]
        [string]$NTName, 
        [parameter()]
        [string]$blnNT
    )
    $Counter++
    If ($blnNT -eq $True) {
        # Convert NetBIOS domain name of group to Distinguished Name.
        $objNT.InvokeMember("Set", "InvokeMethod", $Null, $Translate, (3, ("{0}{1}" -f $NetBIOSDomain.Trim(),$NTName)))
        $DN = $objNT.InvokeMember("Get", "InvokeMethod", $Null, $Translate, 1)
        $ADGroup = [ADSI]"LDAP://$DN"
    } Else {
        $DN = $DomainGroup.distinguishedName
        $ADGroup = $DomainGroup
    }            
    ForEach ($MemberDN In $ADGroup.Member) {
        $MemberGroup = [ADSI]("LDAP://{0}" -f ($MemberDN -replace '/','\/'))
        New-Object PSObject -Property @{
            Computername = $Computer
            Name = $MemberGroup.name[0]
            Type = 'Domain'
            ParentGroup = $NTName
            isGroup = ($MemberGroup.Class -eq "group")
            Depth = $Counter
        }
        # Check if this member is a group.
        If ($MemberGroup.Class -eq "group") {  
            Write-Verbose ("{0} is a group" -f $MemberGroup.name[0])  
            Write-Verbose ("Checking if Counter: {0} is less than Depth: {1}" -f $Counter, $Depth)               
            If ($Counter -lt $Depth) {
                If ($Groups[$MemberGroup.name[0]] -notcontains 'Domain') {
                    Write-Verbose ("{0}: Getting domain group members" -f $MemberGroup.name[0])
                    $Groups[$MemberGroup.name[0]] += ,'Domain'
                    # Enumerate members of domain group.
                    Get-DomainGroupMember $MemberGroup $MemberGroup.Name[0] $False
                }                                                
            }
        }
    }
}

The Get-DomainGroupMember is my second helper function used to get group members. As the name implies, this will gather the group memberships that have been queried. the NetBIOSDomain name is also used here to find out the actual distinguishedName of the group so I can be used with the [ADSI] accelerator to make the query for group members. As with my Get-LocalGroupMember function, this makes use of the same hash table and counter to handle circular groups and recursion depth.

    Process {
        #region Get Local Group Members
        ForEach ($Computer in $Computername) {
            $Script:Groups = @{}
            $Script:Counter=0
            # Bind to the group object with the WinNT provider.
            $ADSIGroup = [ADSI]"WinNT://$Computer/$Group,group"
            Write-Verbose ("Checking {0} membership for {1}" -f $Group,$Computer)
            $Groups[$Group] += ,'Local'
            Get-LocalGroupMember -LocalGroup $ADSIGroup
        }
        #endregion Get Local Group Members
    }

Here is where everything happens for the queries. Everything that exists in the Process block is here for a reason so I am not needlessly making the same variable creation for each computer passed through the pipeline. Each computer will have a fresh counter and new hash table to handle recursion depth and circular groups, respectfully. Because the first group queried is the one that we defined in the Group parameter, it is already known to be a local group and Get-LocalGroupMember is called first.

I also have some runspace stuff in the code as well, but being how I have already talked about this in other articles, I figured it wasn’t worth mentioning again. If you really want to see the code behind the runspaces, check out those articles here.

Ok, enough talk about the code! It is time to see Get-LocalGroupMembership in action.

Get-LocalGroupMembership | Format-Table –AutoSize

image

You will notice that the groups are only nested 2 levels deep. Also important to notice is that I have some circular groups here with Sysops Admins under Enterprise Admins. Sysops Admins which is also listed under Administrators in which Sysops Admins has… Enterprise Admins listed as a member and thus the circle of member begins! Or at least it would have begun had the code not caught it.

I do not have other remote systems to use –Computername but trust me in that it does work against remote systems.

Get-LocalGroupMembership -Depth 1 | Format-Table –AutoSize

image

This is an example of setting the Depth parameter to only go 1 level deep.

That is all for this function. Feel free to download it from the Script Repository (link below) and let me know what you think!

Download

Technet Script Repository

Posted in powershell, scripts | Tagged , , , , , | 40 Comments

Quick Hits: Set the Default Property Display in PowerShell on Custom Objects

Whenever we write a function or a  script and choose what properties to display, we are usually greeted with the usual display of every single property that we used instead of what the the PowerShell cmdlets and other module cmdlets such as ActiveDirectory (to name one) show which is usually 4 or less properties as a table (or list in some cases).

Custom Object

image

What I want it to be

image


This is due to how the .ps1xml formatting files are written to make sure that there are only a small number of properties that will be displayed to the user.

Fortunately, we can accomplish this same type of output without the need of creating a formatting file and loading it up (a module would be a different story, but for a single function I don’t think it should be necessary).

First thing that we need to do is create a custom object (PowerShell V3 way)

$object = [pscustomobject]@{
    FirstName = 'Bob'
    LastName = 'Smith'
    City = 'San Diego'
    State = 'CA'
    Phone = '555-5555'
    Gender = 'Male'
    Occupation = 'System Administrator'
    DOB = '02/21/1970'
}

image

Nothing new here. Just the usual display of all of the properties when calling $object.

Because this is a custom object, I want to give it a unique typename.

#Give this object a unique typename
$object.PSObject.TypeNames.Insert(0,'User.Information')

Next up is to define the default properties which I can to display when calling this object.

#Configure a default display set
$defaultDisplaySet = 'FirstName','LastName','Gender','City'

I now need to create the property set that will be used later on the object to define what will be displayed. I also need to ensure that I specify the label as a ‘DefaultPropertyDisplaySet’ in the object creation.

#Create the default property display set
$defaultDisplayPropertySet = New-Object System.Management.Automation.PSPropertySet(‘DefaultDisplayPropertySet’,[string[]]$defaultDisplaySet)

Now we need to create the member info objects needed by Add-Member.

$PSStandardMembers = [System.Management.Automation.PSMemberInfo[]]@($defaultDisplayPropertySet)

Lastly, we finish up by using Add-Member to add this new member set into the existing custom object.

$object | Add-Member MemberSet PSStandardMembers $PSStandardMembers

Now let’s check out the finished product!

image

Just like I had in the beginning! If I want to see everything then I can just use Select-Object –Property *.

image

Applying this more to a real world scenario, this is how I would end up handling multiple iterations with an object being returned each time.

##Set up the default display set and create the member set object for use later on
#Configure a default display set
$defaultDisplaySet = 'FirstName','LastName','Gender','City'

#Create the default property display set
$defaultDisplayPropertySet = New-Object System.Management.Automation.PSPropertySet(‘DefaultDisplayPropertySet’,[string[]]$defaultDisplaySet)
$PSStandardMembers = [System.Management.Automation.PSMemberInfo[]]@($defaultDisplayPropertySet)

1..10 | ForEach {
    $object = [pscustomobject]@{
        FirstName = (Get-Random -InputObject @('Boe','Bob','Joe','Bill'))
        LastName = (Get-Random -InputObject @('Stevens','Williams','Smith','Prox'))
        City = 'San Diego'
        State = 'CA'
        Phone = '555-5555'
        Gender = 'Male'
        Occupation = 'System Administrator'
        DOB = '02/21/1970'
    }

    #Give this object a unique typename
    $object.PSObject.TypeNames.Insert(0,'User.Information')
    $object | Add-Member MemberSet PSStandardMembers $PSStandardMembers

    #Show object that shows only what I specified by default
    $Object
}

image

What I’ve done is set up most of the default property set up front before I start the actual work and displaying the object in the ForEach statement. This way I am not repeating a process that only has to be done just once.

As you can see, there is not a lot of work involved in doing this and in the end you get a better way of displaying your output without having to mess around with creating or modifying p1xml files!

Posted in powershell, Tips | Tagged , , , , | 11 Comments

PowerShell and Events: WMI Temporary Event Subscriptions

It’s been a while since my last post in my series on PowerShell and Events (February to be exact), but now that I have some projects caught up, it is time to make a return to wrap up this series in what will be 2 or 3 articles, including this one.

Something that I consider to be a powerful technique is using WMI events to monitor some part a system such as disk space, processes or even the event logs. The nice part about this is that you can configure some sort of action to occur when this is tripped such as an email alert or just a display on the console. Considering the massive scope of the WMI repository, you can see that the options are almost limitless with what you can monitor and report on!

Before I show off some examples, we first need to know what is used as far as classes go and how we even get to the point of setting up a temporary subscription.

What do you mean temporary?

When creating a WMI event in PowerShell, you are merely creating something that exists only in the PowerShell console that is running as a background job. This means that as soon as you close the console, the event monitoring goes away with it. Same for rebooting the system and removing the background job or using Unregister-Event to stop the event subscription. Obviously not a permanent monitoring solution by any means, but it does get the job done if you want something that isn’t meant to last forever (or a day Winking smile).

Cmdlets used for WMI Events

Really, the only cmdlet that is required for creating a WMI event is Register-Event. This cmdlet will return a background job object showing that it is now performing the monitoring that you specified and will also perform an action as well if specified. This cmdlet has the same type of parameters as Register-ObjectEvent and Register-EngineEvent, both of which I have explained in detail here and here. Refer to these for more on the parameters such as –Forward, –MessageData and –SupportEvent. If you have used either of the other Register* cmdlets before, then you should feel closer to home as far as the syntax goes. The big thing to grasp is the WMI filter query used to register the event.

Useful classes to know

Working with WMI Events means that you need to know what kind of classes are available to initiate the subscriptions. The following classes are the ones that I have used or read about and will prove useful when registering for the WMI Events:

Win32_ProcessStartTrace http://msdn.microsoft.com/en-us/library/windows/desktop/aa394374(v=vs.85).aspx
Win32_ProcessStopTrace http://msdn.microsoft.com/en-us/library/windows/desktop/aa394376(v=vs.85).aspx
__InstanceModificationEvent http://msdn.microsoft.com/en-us/library/windows/desktop/aa394651(v=vs.85).aspx
__InstanceCreationEvent http://msdn.microsoft.com/en-us/library/windows/desktop/aa394649(v=vs.85).aspx
__InstanceDeletionEvent http://msdn.microsoft.com/en-us/library/windows/desktop/aa394650(v=vs.85).aspx
RegistryTreeChangeEvent http://msdn.microsoft.com/en-us/library/windows/desktop/aa393041(v=vs.85).aspx
RegistryKeyChangeEvent http://msdn.microsoft.com/en-us/library/windows/desktop/aa393040(v=vs.85).aspx
RegistryValueChangeEvent http://msdn.microsoft.com/en-us/library/windows/desktop/aa393042(v=vs.85).aspx

Working down the line of classes, the ProcessTrace classes are used to report when a process has started or stopped.

The __Instance* classes are intrinsic event classes which reports a change to the WMI repository, such as a creation or modification. An example of this would be to use one of the __Instance* classes with Win32_Service to track when a service starts or stops. More on this with examples later.

Lastly on this list, the Registry* classes are extrinsic events which cannot be linked directly to the WMI model. Regardless, you can use this to receive a notification if a part of the registry that has already been defined has been modified of deleted.

With all of these classes that can be used in various combinations, you could say that possibilities on what can be monitored are endless!

I won’t be covering all of these classes but from the examples that I show you later, you can use the other classes to perform various types of monitoring.

Windows Query Language (WQL)

In order to configure the event monitoring with WMI, you must know how to use WQL to setup the filter so the event subscription actually knows what it is looking for. Here is one example:

"select * from __instanceModificationEvent within 5 where targetInstance isa 'win32_Service'"

Breaking it down, I will show you what is happening with this filter.

“select * from __instanceModificationEvent” Specify properties which are returned from query.
“within 5” Poll every 5 seconds for event (this doesn’t mean something will be missed, it just means that it may take 5 seconds to acknowledge the event).
“where targetInstance isa ‘win32_Service'” Where filters the scope down and isa applies a query to the subclasses of a specified class

 

For more information regarding WMI queries, check out the following article on more keywords related to WQL: http://msdn.microsoft.com/en-us/library/windows/desktop/aa394606(v=vs.85).aspx

Example using Win32_Service

I already began with my query above using Win32_Service, so lets flesh this out and make an actual subscription using Register-WMIEvent. Some of this may seem familiar if you have checked out my other eventing articles.

##Service change
$WMI = @{
    Query = "select * from __instanceModificationEvent within 5 where targetInstance isa 'win32_Service'"
    Action = {
            If ($Event.SourceEventArgs.NewEvent.PreviousInstance.State -ne $event.SourceEventArgs.NewEvent.TargetInstance.State) {
            $Global:Data = $Event
            Write-Host ("Service: {0}({1}) changed from {2} to {3}" -f $event.SourceEventArgs.NewEvent.TargetInstance.DisplayName,
                                                                            $event.SourceEventArgs.NewEvent.TargetInstance.Name,
                                                                            $event.SourceEventArgs.NewEvent.PreviousInstance.State,
                                                                            $event.SourceEventArgs.NewEvent.TargetInstance.State) -Back Black -Fore Green
            }
    }
    SourceIdentifier = "Service.Action"
}
$Null = Register-WMIEvent @WMI

Nothing really different here, I am using splatting to setup my WMI parameters for Register-WMIEvent cmdlet. I use the same query as mentioned above for the filtering and then my action statement is a scriptblock which tells the subscription what to do when an event is tripped. The action block is set in its own scope and by that I mean when it uses the automatic variable $Event, it is limited only to that script block. You can work around this by setting a Global variable to save $Event to so it can be viewed later.

image

There is a lot happening here with this particular object for the Win32_Service class and the change of the Firewall service from Running to Stopped. From the picture above, you can see where the Action block specified in the code wrote a message on screen saying that the service went from a Running to Stopped state. But how was I able to figure this out? Diving into the $Data variable that was created to hold the Event object, we can begin to see where all of this data resides.

$Data.SourceEventArgs.NewEvent

image

Looking at the SourceEventArgs.NewEvent property, you can see the TIME_CREATED which lets us know when the service change took place. There are two other properties which hold a lot of value to me: PreviousInstance and TargetInstance. The PreviousInstance shows the state of the service prior to a change while the TargetInstance shows the current state of the service. This is why I chose to use an If statement in my Action scriptblock to only worry about the State changes. If I had not done this, I would receive notifications for other changes in services, such as the startmode changing or something else.

PreviousInstance (Previous State)

image

TargetInstance (Current state)

image

Knowing this, I can easily show the previous state of the service as well as show its current state in my display. By the way, the TargetInstance object returned is not a live WMI object, in this case it is System.Management.ManagementBaseObject#root\CIMV2\Win32_Service. But what happens if I try to use the Start() method of this object? Watch and see.

image

If I wanted to restart this service, then I would either use Invoke-WMIMethod or Start-Service using the service name given to achieve that goal. Cool, right? Lets see an example using the Win32_ProcessStartTrace and Win32_ProcessStopTrace to track the processes on my system.

Process Watcher

This example uses the same Register-WMIEvent cmdlet but instead of working with the __Instance* classes for the intrinsic events, I will instead use the readily available Process*Trace classes instead to track processes which have started and stopped running on my local system.

##New Process
$WMI = @{
    Query = "select * from Win32_ProcessStartTrace"
    Action = {
        Write-Host ("Process: {0}({1}) started at {2}" -f $event.SourceEventArgs.NewEvent.ProcessName,
                                                        $event.SourceEventArgs.NewEvent.ProcessID,
                                                        [datetime]::FromFileTime($event.SourceEventArgs.NewEvent.TIME_CREATED)) -Back Black -Fore Green
    }
    SourceIdentifier = "Process.Created"
}
$Null = Register-WMIEvent @WMI
 
##Process End
$WMI = @{
    Query = "select * from Win32_ProcessStopTrace"
    Action = {
        Write-Host ("Process: {0}({1}) was terminated at {2}" -f $event.SourceEventArgs.NewEvent.ProcessName,
                                                                $event.SourceEventArgs.NewEvent.ProcessID,
                                                                [datetime]::FromFileTime($event.SourceEventArgs.NewEvent.TIME_CREATED)) -Back Black -Fore Yellow
    }
    SourceIdentifier = "Process.Deleted"
}
$Null = Register-WMIEvent @WMI

Since my system is not really active right now, I have some self-inflicted processes starting and stopping to give you an idea as to what you will see on the console.

image

With some extra work, this could be a very simple procmon to track what is running.

Tracking the Event Log for Local Account Creations

The last example in the __Instance* classes are going to highlight monitoring the event log on a local system for a account creation attempt. This is being done on a Windows 2012 server so the event log ids may be different if running a 2003 server. In this case, the event id is 4720 for a new account creation (same id for 2008).  What is interesting is that even if I input a password that doesn’t meet the complexity requirements, it will actually create the account, attempt to reset the password to the password given, then upon failure remove the account. With that, lets kick off the monitoring.

##Event log watch -- New User Creation on local system
$WMI = @{
    Query = "select * from __InstanceCreationEvent where TargetInstance isa 'Win32_NtLogEvent' and TargetInstance.logfile = 'Security' and (TargetInstance.EventCode = '4720')"
    Action = {
        $AccountCreated = $event.SourceEventArgs.NewEvent.TargetInstance.insertionstrings[0]
        $CreatedBy = ("{0}\{1}" -f $event.SourceEventArgs.NewEvent.TargetInstance.insertionstrings[5],$event.SourceEventArgs.NewEvent.TargetInstance.insertionstrings[4])
        Write-Host -Foreground Green -Back Black ('New Account: {0} was created by: {1}' -f $ACcountCreated,$CreatedBy)
        $Global:data = $Event
    }
    SourceIdentifier = "Account.Created"
}
$Null = Register-WMIEvent @WMI
 

 

image

Works like a champ! Diving deeper into the object reveals more information about the account itself:

image

As I did with my little console alert, you can narrow down the scope of what you want to report on just by picking what properties are important to you. With the event log monitoring, you have a pretty robust way of monitoring any event that you want on a system.

I’ve shown examples for the Instance* and Process*Trace events and now I will wrap this up with a Registry* class example just to give you an idea as to what it is capable of.

Registry Monitoring

This example will demonstrate how to monitor a registry tree using the RegistryChangeEvent class for the extrinsic event.

##Registry Change
$WMI = @{
    Query ="Select * from RegistryTreeChangeEvent where Hive='HKEY_LOCAL_MACHINE' AND RootPath='Software\\'"
    Action = {
        $Global:Data = $Event
        Write-Host 'Something happened!' -ForegroundColor Green -BackgroundColor Black
    }
    SourceIdentifier = "RegistryTree.Changed"
}
$Null = Register-WMIEvent @WMI

I can make a change anywhere down the HKLM:\Software tree and it will register an event.

image

What I ended up doing is deleting a registry value under Software\Test and that tripped the event. What you can’t see is what was deleted or where it was deleted at! Kind of unfortunate but at least it is something.

The truth is that this is the same type of output you will receive with the other Registry*ChangeEvent classes. The difference is that you have to fine-tune what you are looking at with either the Key or Value that you wish to monitor. At least with these you can tie in some more granular action block items using Get-Item or Get-ItemProperty under the registry provider to verify the new values or check to see if the Key or Value has been removed.

As shown in this article, using WMI temporary events can prove to be a useful thing if you just need a quick monitor with no care about what happens if the console closes or something else happens that causes the event monitoring to stop. While this wasn’t an exhaustive article on temporary WMI events, I hope that it will provide a decent road map in your future scripts to utilize this technique.

My last article regarding PowerShell and Events will conclude with working with permanent WMI events and how they will survive a reboot and keep on monitoring.  It won’t be my next article because I have others already planned to work on, but it will get done!

Posted in powershell | Tagged , , , | 4 Comments

Quick Hits: Determine Tombstone Lifetime in Active Directory

Recently, I wanted to know what the tombstone lifetime was in my environment and decided to find this using PowerShell. Given, I could have done something with dsquery or dug in using the ADSI type accelerator to connect to my domain controller and dig through to find it.

For those of you unfamiliar with this attribute, a good explanation of this is

The number of days before a deleted object is removed from the directory services. This assists in removing objects from replicated servers and preventing restores from reintroducing a deleted object.

Basically, I wanted to know how long I had to recover  if (in my case) one of my domain controllers were down for an extended period of time. For more information on the fun that can occur if this happens and it is down beyond the tombstone lifetime, check out this article: http://technet.microsoft.com/en-us/library/cc786630(v=ws.10).aspx

But back to my question, I already know a number of ways to get this information, but wanted to see if this can be done using the ActiveDirectory module. And the answer is a resounding Duh! Smile This is PowerShell and the ActiveDirectory team has done a fine job with their module which make accessing this attribute an easy issue using the Get-ADObject cmdlet.

In fact, it is so simple it can be done with one line!

(get-adobject "cn=Directory Service,cn=Windows`
 NT,cn=Services,cn=Configuration,dc=rivendell,dc=com" `
-properties "tombstonelifetime").tombstonelifetime

Yes, I am using backticks in my code listing (bad practice!) but I wanted this to fit in the window with no scrolling required. But, as you can see here, the result is exactly what I was looking for! Of course, you will want to change it where I have dc=rivendell,dc=com to whatever matches your environment.

image

There you have it! A nice way to determine your tombstone lifetime using PowerShell and the ActiveDirectory module!

Posted in powershell, Tips | Tagged , , , | 3 Comments

PowerShell Deep Dives Book Now Available!

That is right! After a lot of hard work, the PowerShell Deep Dives book will be available (Print edition) on 26 July with the e-book available now. As one of the co-authors of this book, I can tell you that not only has this been a challenging task as I did a lot of research into things that I already know just to be sure that I really did know it, but it was also one of the most enjoyable things that I have had the pleasure of doing for the PowerShell community. All proceeds to this book are being donated to the Save The Children charity.

So not only are you getting an amazing book filled page to page with some amazing PowerShell content, but you are also contributing to a great cause as well! This is really a great book and one that you should have in your library!

Because they all deserve mention, the authors of this book are:

Chris Bellée, Bartek Bielawski, Robert C. Cain, Jim Christopher, Adam Driscoll, Josh Gavant, Jason Helmick, Don Jones, Ashley McGlone, Jonathan Medd, Ben Miller, James O’Neill, Arnaud Petitjean, Vadims Podans, Karl Prosser, Boe Prox, Matthew Reynolds, Mike Robbins, Donabel Santos, Will Steele, Trevor Sullivan, and Jeff Wouters

And of course the editors who made sure that everything we wrote sounded good and was free of any issues are:

Jeffery Hicks, Richard Siddaway, Oisín Grehan, and Aleksandar Nikolić

Big thanks to Jeffery Hicks for putting all of this together and keeping us all in step to make sure that this became a reality!

Hope everyone enjoys this book as much as we did putting it together!

Posted in Deep Dive, News, powershell | Tagged , , | Leave a comment