How to Find Active Directory Schema Update History by Using PowerShell

How to Find Active Directory Schema Update History by Using PowerShell

  • Comments 17
  • Likes

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

Your comment has been posted.   Close
Thank you, your comment requires moderation so it may take a while to appear.   Close
Leave a Comment
  • Cool script. I just finished digesting it and decided I wanted to add an entry for System Center Configuration Manager. However after checking the CN=mS-SMS-Version attribute in ADSI Edit I noticed that there is no entry for rangeLower or rangeUpper. Is there any other attribute in the schema that could be queried against that would provide us with a way to determine the SCCM version?

  • Hi DLetsinger,

    I was smiling as I read your comment.  I actually investigated this and wanted to include SCCM also.  Unfortunately this is not as easy as the other product versions.  I am not an SCCM expert, but I have a friend who is.  He told me there is probably a way to track down SMS/SCCM versions from management point objects published in AD.  If you can script it please publish an update to the TechNet Script Gallery.

    Thanks,

    Ashley

    @GoateePFE

  • I love this but when I invoke it remotely from my RMM tool the output is truncated due to the display screen not being large enough. I tired to add a convertto-html to convert the output to html but I still get this error due to the display not being wide enough so all I can see is the output for the names. How hard would it be to convert this over to pure html output - I didnt see any format-list commands so there was nothing to convert to select-object that I could find.

    My goal is to run thsi remotely via script and grab the output via html for review.

  • Nice ! Thanks.

  • This script is great. Is there any change you could please add checks for MOM and SCOM?

    Thanks,

    Amelia

  • Amelia, there are no schema updates for SCOM.

  • I have updated the script for the most recent schema product versions, and I have added a CSV output of the schema attribute and class data.  Enjoy!

    gallery.technet.microsoft.com/.../PowerShell-Active-4ffedca4

  • I just downloaded what appears to be the latest version, but Exchange 2013 is still only at RTM. I added CU1, CU2, and CU3 version numbers to the hash:

    $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";

       14625="Exchange 2007 SP3";

       14726="Exchange 2010 SP1";

       14732="Exchange 2010 SP2";

       14734="Exchange 2010 SP3";

       15137="Exchange 2013 RTM";

       15254="Exchange 2013 CU1";

       15281="Exchange 2013 CU2";

       15283="Exchange 2013 CU3"

       }

  • In addition to Shawn's info, 15254 can include the beta of CU1. For Lync, 1150 is for Lync Server 2013. For AD, I have an environment that shows as 69 that I'm trying to figure out what it relates to.

  • For those curious on getting the SCCM Version. I think this will do it. (Only tested against 2012R2 though). This will likely only work with a single Site in the domain. Got the Build list from Wikipedia:

    $SchemaHashSCCM = @{
    "4.00.5135.0000"="SCCM 2007 Beta 1";
    "4.00.5931.0000"="SCCM 2007 RTM";
    "4.00.6221.1000"="SCCM 2007 SP1/R2";
    "4.00.6221.1193"="SCCM 2007 SP1 (KB977203)";
    "4.00.6487.2000"="SCCM 2007 SP2";
    "4.00.6487.2111"="SCCM 2007 SP2 (KB977203)";
    "4.00.6487.2157"="SCCM 2007 R3";
    "4.00.6487.2207"="SCCM 2007 SP2 (KB2750782)";
    "5.00.7561.0000"="SCCM 2012 Beta 2";
    "5.00.7678.0000"="SCCM 2012 RC1";
    "5.00.7703.0000"="SCCM 2012 RC2";
    "5.00.7711.0000"="SCCM 2012 RTM";
    "5.00.7711.0200"="SCCM 2012 CU1";
    "5.00.7711.0301"="SCCM 2012 CU2";
    "5.00.7782.1000"="SCCM 2012 SP1 Beta";
    "5.00.7804.1000"="SCCM 2012 SP1";
    "5.00.7804.1202"="SCCM 2012 SP1 CU1";
    "5.00.7804.1300"="SCCM 2012 SP1 CU2";
    "5.00.7804.1400"="SCCM 2012 SP1 CU3";
    "5.00.7804.1500"="SCCM 2012 SP1 CU4";
    "5.00.7958.1000"="SCCM 2012 R2"
    }

    $SchemaPathSCCM = "CN=System Management," + (Get-ADDomain).SystemsContainer
    if (Test-Path "AD:$SchemaPathSCCM") {
    $SCCMData = Get-ADObject -SearchBase ("CN=System Management," + (Get-ADDomain).SystemsContainer) -LDAPFilter "(&(objectClass=mSSMSManagementPoint))" -Property mSSMSCapabilities,mSSMSMPName
    $SCCMxml = [XML]$SCCMdata.mSSMSCapabilities
    $schemaVersionSCCM = $SCCMxml.ClientOperationalSettings.Version
    }Else{
    $schemaVersionSCCM = 0
    }

    $SchemaVersions += 1 | Select-Object @{name="Product";expression={"SCCM"}}, @{name="Schema";expression={$schemaVersionSCCM}}, @{name="Version";expression={$SchemaHashSCCM.Item($schemaVersionSCCM)}}


    Let me know if it works for you!

  • Windows Server Build 56 is 2012. Build 69 is 2012 R2.
    http://msdn.microsoft.com/en-us/library/cc223174.aspx

  • Paul,
    I needed to make a small update to your SCCM code for our case because we have more than one version ($SCCMData contained 3 objects in our environment). This sets $schemaVersionSCCM to the largest version number if there is more than 1.

    $SchemaPathSCCM = "CN=System Management," + (Get-ADDomain).SystemsContainer
    if (Test-Path "AD:$SchemaPathSCCM") {
    $SCCMData = Get-ADObject -SearchBase ("CN=System Management," + (Get-ADDomain).SystemsContainer) -LDAPFilter "(&(objectClass=mSSMSManagementPoint))" -Property mSSMSCapabilities,mSSMSMPName
    $schemaVersionSCCMList = foreach ($SCCMInstance in 0..($SCCMdata.count -1)) {
    $SCCMxml = [XML]$SCCMdata[$SCCMInstance].mSSMSCapabilities
    $SCCMxml.ClientOperationalSettings.Version
    }
    $schemaVersionSCCM = $schemaVersionSCCMList | Sort-Object -Descending | Select-Object -First 1
    } Else {
    $schemaVersionSCCM = 0
    }

  • This script is exactly what I'm looking for. But when I run it I'm getting an set of errors in each section.
    Missing expression after ','.
    At line:1 char:50
    + @{name="Schema";expression={$SchemaVersionLync}}, <>
    + CategoryInfo : ParserError: (,:String) [], ParentContainsErrorRecordException
    + FullyQualifiedErrorId : MissingExpressionAfterToken

    Not a powershell expect so could someone point me in the right direction to get this resolved.

  • Hey Steve, thanks for sharing the update. I knew that multiple versions of SCCM might cause an issue. I just didn't have anything to test with.

    Also to all:
    Exchange 2013 SP1 is build 15292

  • Steve, I took your SCCM change one step further as it no longer accounted for a single object. So, I had it check data type. This is my updated code:

    $SchemaPathSCCM = "CN=System Management," + (Get-ADDomain).SystemsContainer
    if (Test-Path "AD:$SchemaPathSCCM") {
    $SCCMData = Get-ADObject -SearchBase ("CN=System Management," + (Get-ADDomain).SystemsContainer) -LDAPFilter "(&(objectClass=mSSMSManagementPoint))" -Property mSSMSCapabilities,mSSMSMPName
    if ($sccmdata -isnot [system.Array]) {
    $SCCMxml = [XML]$SCCMdata.mSSMSCapabilities
    $schemaVersionSCCM = $SCCMxml.ClientOperationalSettings.Version
    } else {
    $schemaVersionSCCMList = Foreach($SCCMINstance in 0..($SCCMData.count -1)) {
    $SCCMxml = [XML]$SCCMdata[$SCCMInstance].mSSMSCapabilities
    $SCCMxml.ClientOperationalSettings.Version
    }
    $SchemaVersionSCCM = $schemaVersionSCCMList|Sort-Object -Descending|Select-Object -First 1
    }
    }Else{
    $schemaVersionSCCM = 0
    }

    Enjoy!