How to Find Active Directory Schema Update History by Using PowerShell

Summary: Use Windows PowerShell to discover what schema updates have been applied to your Active Directory environment.

Microsoft Scripting Guy, Ed Wilson, is here. Today we have as our guest blogger, Ashley McGlone. Ashley is a premier field engineer for Microsoft. He started writing code on a Commodore VIC20 back in 1982, and he’s been hooked ever since. Today he specializes in Active Directory and PowerShell, helping Microsoft Premier Customers reach their full potential through risk assessments and workshops. Ashley’s favorite workshop to teach is Windows PowerShell Essentials, and his TechNet blog focuses on using Windows PowerShell with Active Directory.
Blog: Goatee PFE
Twitter: @GoateePFE

Take it away Ashley…

Where am I? How did I get here?

Marvel X-Men fans know that Wolverine's character is interesting because of his mysterious past. Those unfamiliar with the comics had to wait until the Wolverine movie to find out exactly why he couldn't remember where he came from. After seeing the movie, I thought he's better off not knowing the tortured past.

Some Active Directory (AD) admins are a bit like Wolverine…razor claws aside. They have hired into an IT shop where the former admin is nowhere to be found, and they need help finding out the mysterious past of their AD environment. What schema updates have been applied? Where has delegation been granted? And why is there a user account called "DO NOT DELETE"?

Today's post offers some simple scripts to document the history of schema updates. This is particularly handy when it comes time to extend the schema for a domain upgrade or Exchange implementation. Now you can get a report of every attribute's create and modified date. You can also find out if and when third-party extensions have been applied.

When did all this happen?

To report on schema updates, we simply dump all of the objects in the schema partition of the Active Directory database and group by the date created. This script does not call out updates by name, but you can infer from the schema attributes that are listed which update was applied. For example, if you see a day with a bunch of Exchange Server attributes added, then that was one of the Exchange Server upgrades or service packs. The same is true for AD forest preps, OCS/Lync, SMS/SCCM, and so on. Then based on the affected attributes and dates, you can extrapolate the product version involved.

It is entirely possible that later schema updates modified previously created attributes. Note that the Windows Server 2008 R2 forest prep hits nearly every attribute in the database when it adds the Filtered Attribute Set (FAS) for RODCs. As a result, we cannot trust the WhenModified attribute to show us a true history. Therefore, in the report, we use the WhenCreated attribute and show the WhenModified date for added flavor.

Windows PowerShell

Although this code is not much more than a Get-ADObject, I want to look at the two different grouping techniques. Get-Help provides the following information:

Format-Table -GroupBy

Group-Object

Arranges sorted output in separate tables based on a property value. For example, you can use GroupBy to list services in separate tables based on their status. The output must be sorted before you send it to Format-Table.

The Group-Object cmdlet displays objects in groups based on the value of a specified property. Group-Object returns a table with one row for each property value and a column that displays the number of items with that value.

Notice in the output that Format-Table -GroupBy shows you the data inside each grouping, while Group-Object gives you a count of the items within the grouping. This is an important distinction, and most folks aren't aware of this little switch with Format-Table. Also, note that Group-Object creates its own column names (Count, Name, Group).

Import-Module ActiveDirectory

$schema = Get-ADObject -SearchBase ((Get-ADRootDSE).schemaNamingContext) `
-SearchScope OneLevel -Filter * -Property objectClass, name, whenChanged,`
whenCreated | Select-Object objectClass, name, whenCreated, whenChanged, `
@{name="event";expression={($_.whenCreated).Date.ToShortDateString()}} | `
Sort-Object whenCreated

"`nDetails of schema objects changed by date:"
$schema | Format-Table objectClass, name, whenCreated, whenChanged `
-GroupBy event -AutoSize

"`nCount of schema objects changed by date:"
$schema | Group-Object event | Format-Table Count, Name, Group –AutoSize

The following image illustrates the schema objects with the date that they were created and when they changed.

Image of schema objects

The image shown here illustrates a total count of the schema objects created by date.

Image of schema objects

Your results will appear much more interesting than these from my sterile lab environment.

Was your forest really created in the year 1630?

When I first wrote this script, I assumed that the oldest attribute date in the schema report would be the creation date of the forest. That was a wrong assumption. After testing this code in a number of different environments, I found that all forests created on Windows Server 2008 R2 shared a common date in 2009 for the oldest created schema attribute. To make things even more interesting, forests created on Windows 2000 Server show dates from the year 1630 on their oldest attributes. I knew this couldn't be correct, so I had to find out where the dates originated.

The answer lies in the DCPROMO process. When you promote a new domain controller, it creates the database file from a template like the one shown here:

Template database

%systemroot%\System32\NTDS.dit

Default install location

%systemroot%\NTDS\NTDS.dit

Here is a quote from the TechNet topic How the Active Directory Installation Wizard Works:

"When you install Active Directory on a computer that is going to be the root of a forest, the Active Directory Installation Wizard uses the default copy of the schema and the information in the schema.ini file to create the new Active Directory database."

As a result, the WhenCreated dates of the initial schema attributes when a forest is built come from the template database, and they are not valid values. Ignore them.

How to find the forest creation date

To locate the actual installation date of the forest (and all of the domains), we can query the CrossRef objects in the Configuration partition. The applicable objects seen in ADSI Edit are shown in the following image.

Image of objects

The following script shows how to find these CrossRef objects.

Import-Module ActiveDirectory

Get-ADObject -SearchBase (Get-ADForest).PartitionsContainer `
-LDAPFilter "(&(objectClass=crossRef)(systemFlags=3))" `
-Property dnsRoot, nETBIOSName, whenCreated |
Sort-Object whenCreated |
Format-Table dnsRoot, nETBIOSName, whenCreated -AutoSize

In the query, we specify that we only want CrossRef objects with a SystemFlags value of 3, which includes all partitions that are domains (excluding other partitions like DNS). Now we have a list of all domains in the forest and their creation date. Obviously, the root domain is the oldest, and it represents the forest creation date. Here is a screenshot from my lab:

Image of command output

Although this data does not come from the schema partition, it is a quick and reliable way to know when the forest domains were created.

How can I know the current product versions from schema data?

The next logical question after looking at the schema report is, "What is my current forest schema version?" This one is easy to answer with another simple Get-ADObject query. But why stop there? Let's also grab the Exchange Server and Lync versions of the schema as follows.

#——————————————————————————

Import-Module ActiveDirectory

$SchemaVersions = @()

$SchemaHashAD = @{
13="Windows 2000 Server";
30="Windows Server 2003";
31="Windows Server 2003 R2";
44="Windows Server 2008";
47="Windows Server 2008 R2"
}

$SchemaPartition = (Get-ADRootDSE).NamingContexts | Where-Object {$_ -like "*Schema*"}
$SchemaVersionAD = (Get-ADObject $SchemaPartition -Property objectVersion).objectVersion
$SchemaVersions += 1 | Select-Object `
@{name="Product";expression={"AD"}}, `
@{name="Schema";expression={$SchemaVersionAD}}, `
@{name="Version";expression={$SchemaHashAD.Item($SchemaVersionAD)}}

#——————————————————————————

$SchemaHashExchange = @{
4397="Exchange Server 2000 RTM";
4406="Exchange Server 2000 SP3";
6870="Exchange Server 2003 RTM";
6936="Exchange Server 2003 SP3";
10628="Exchange Server 2007 RTM";
10637="Exchange Server 2007 RTM";
11116="Exchange 2007 SP1";
14622="Exchange 2007 SP2 or Exchange 2010 RTM";
14726="Exchange 2010 SP1";
14732="Exchange 2010 SP2"
}

$SchemaPathExchange = "CN=ms-Exch-Schema-Version-Pt,$SchemaPartition"
If (Test-Path "AD:$SchemaPathExchange") {
$SchemaVersionExchange = (Get-ADObject $SchemaPathExchange -Property rangeUpper).rangeUpper
} Else {
$SchemaVersionExchange = 0
}

$SchemaVersions += 1 | Select-Object `
@{name="Product";expression={"Exchange"}}, `
@{name="Schema";expression={$SchemaVersionExchange}}, `
@{name="Version";expression={$SchemaHashExchange.Item($SchemaVersionExchange)}}

#——————————————————————————

$SchemaHashLync = @{
1006="LCS 2005";
1007="OCS 2007 R1";
1008="OCS 2007 R2";
1100="Lync Server 2010"
}

$SchemaPathLync = "CN=ms-RTC-SIP-SchemaVersion,$SchemaPartition"
If (Test-Path "AD:$SchemaPathLync") {
$SchemaVersionLync = (Get-ADObject $SchemaPathLync -Property rangeUpper).rangeUpper
} Else {
$SchemaVersionLync = 0
}

$SchemaVersions += 1 | Select-Object `
@{name="Product";expression={"Lync"}}, `
@{name="Schema";expression={$SchemaVersionLync}}, `
@{name="Version";expression={$SchemaHashLync.Item($SchemaVersionLync)}}

#——————————————————————————

"`nKnown current schema version of products:"
$SchemaVersions | Format-Table * -AutoSize

#—————————————————————————><>

I've included a number of links to articles that document these schema versions and locations at the end of this post. Here is an example of the output:

Image of command output

By using the previous template code, you can add additional schema version checks for other product extensions in your environment.

This blog is for all IT Pros who have inherited an Active Directory environment that they did not build. Now you have some insight on the origins of your directory. While you may not have adamantium fused to your skeleton, you can now use AD-PowerShell-ium to understand a bit of your broken past.

Additional resources

You can download the full script from the Microsoft. TechNet Gallery: PowerShell Active Directory Schema Update Report.

~Ashley

Thank you, Ashley, for taking time to write the guest blog today and sharing your insights with our readers. Join us tomorrow when guest blogger, Rich Prescott, will talk about the Windows PowerShell community and the sysadmin tool. It will be another excellent guest blog.

I invite you to follow me on Twitter and Facebook. If you have any questions, send email to me at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.

Ed Wilson, Microsoft Scripting Guy