Public/Object/Get-ADObjectMetadata.ps1
|
<#
.SYNOPSIS Get the replication metadata of an Active Directory object: which attribute changed, when, and on which domain controller. .DESCRIPTION Reads the msDS-ReplAttributeMetaData constructed attribute, which the domain controller builds on the fly and which records, for every attribute of the object, the version number, the date of the last originating change and the domain controller where that change was made. This is the only reliable way to answer "when was this account disabled, and by which DC was the change written", long after whenChanged has been overwritten by another modification. The metadata is local to the domain controller answering the query. Two DCs can report different values for the same object, which is exactly what makes -Server useful when investigating a replication problem. Linked attributes are out of scope: 'member', 'manager', 'managedBy' and the like are replicated as linked values, so their history lives in msDS-ReplValueMetaData and never shows up here. Group membership is read with Get-ADGroupMembershipMetadata. 'memberOf' carries no metadata at all, being a back-link computed from the 'member' attribute of the groups. Requires the ActiveDirectory module (RSAT). .PARAMETER Identity The distinguished name of the object. Any other form (sAMAccountName, common name, display name) is accepted as a fallback: the object is then searched with an ambiguous name resolution (anr) filter, which must match exactly one object. .PARAMETER Attributes Restricts the output to these attribute names. An attribute the object carries no metadata for is reported with a warning rather than as an empty row. .PARAMETER Server Domain controller to query. Defaults to a discovered DC of the current domain. .PARAMETER OnlyAttributesWithRecentChanges Keeps only the attributes whose last originating change is more recent than -Days. .PARAMETER Days Number of days used by -OnlyAttributesWithRecentChanges. Default is 10. .PARAMETER OnlyUpdatedAttributes Keeps only the attributes whose version is greater than 1, that is, the attributes modified at least once since the object was created. .EXAMPLE Get-ADObjectMetadata 'CN=John Doe,OU=Users,DC=example,DC=com' Every attribute of the object, with the date and origin of its last change. .EXAMPLE Get-ADObjectMetadata jdoe -Attributes userAccountControl, pwdLastSet Resolves jdoe with an anr search, then reports when the account state and the password were last changed. .EXAMPLE Get-ADObjectMetadata 'CN=John Doe,OU=Users,DC=example,DC=com' -OnlyAttributesWithRecentChanges -Days 5 What changed on the object over the last five days. .EXAMPLE Get-ADObjectMetadata 'CN=Domain Admins,CN=Users,DC=example,DC=com' -Server 'dc02.example.com' -OnlyUpdatedAttributes The attributes modified at least once, as dc02 knows them. Comparing the output with another DC shows what has not replicated yet. .LINK https://github.com/bastienperez/PSADDS #> function Get-ADObjectMetadata { [CmdletBinding()] [OutputType([PSCustomObject])] param ( [Parameter(Mandatory = $true, Position = 0)] [Alias('ObjectDN', 'DistinguishedName')] [string]$Identity, [Parameter(Mandatory = $false, Position = 1)] [string[]]$Attributes, [Parameter(Mandatory = $false)] [Alias('DomainController')] [string]$Server, [Parameter(Mandatory = $false)] [switch]$OnlyAttributesWithRecentChanges, [Parameter(Mandatory = $false)] [ValidateRange(1, [int]::MaxValue)] [int]$Days = 10, [Parameter(Mandatory = $false)] [switch]$OnlyUpdatedAttributes ) if (-not $Server) { $Server = (Get-ADDomainController -Discover -Service ADWS).HostName Write-Verbose "No domain controller specified, using the discovered one: $Server" } [System.Collections.Generic.List[PSObject]]$objectMetadata = @() try { $adObject = Get-ADObject -Identity $Identity -Properties 'msDS-ReplAttributeMetaData' -Server $Server -ErrorAction Stop } catch { Write-Verbose "'$Identity' is not a distinguished name, searching the object with (anr=$Identity)" try { $adObject = @(Get-ADObject -LDAPFilter "(anr=$Identity)" -Properties 'msDS-ReplAttributeMetaData' -Server $Server -ErrorAction Stop) } catch { Write-Warning "Unable to search '$Identity' with (anr=$Identity): $($_.Exception.Message)" return } if ($adObject.Count -gt 1) { Write-Warning "Several objects match (anr=$Identity), use the exact distinguished name: $($adObject.DistinguishedName -join ', ')" return } if (-not $adObject) { Write-Warning "No object found for '$Identity', neither as a distinguished name nor with (anr=$Identity)" return } $adObject = $adObject[0] Write-Verbose "Object found: $($adObject.DistinguishedName)" } $replAttributeMetaData = ConvertFrom-ADReplMetadata -RawMetadata $adObject.'msDS-ReplAttributeMetaData' if (-not $replAttributeMetaData) { Write-Warning "No replication metadata returned by $Server for $($adObject.DistinguishedName)" return } # linked attributes never appear in msDS-ReplAttributeMetaData, so an empty result on one of them means # "look at the linked value metadata", not "the attribute is not set" $linkedAttributes = 'member', 'memberOf', 'manager', 'directReports', 'managedBy', 'managedObjects', 'msDS-ManagedBy', 'msDS-ManagedByObjects', 'msDS-NC-RO-Replica-Locations', 'siteObject', 'siteObjectBL' if ($Attributes) { $selectedMetadata = foreach ($attribute in $Attributes) { $attributeMetadata = $replAttributeMetaData | Where-Object { $_.pszAttributeName -eq $attribute } if (-not $attributeMetadata) { if ($attribute -eq 'member') { Write-Warning "'member' is a linked attribute: its history is carried by msDS-ReplValueMetaData, not by msDS-ReplAttributeMetaData. Use Get-ADGroupMembershipMetadata '$($adObject.DistinguishedName)' instead." } elseif ($attribute -eq 'memberOf') { Write-Warning "'memberOf' is a back-link, computed from the 'member' attribute of the groups, and carries no metadata of its own. The history of a membership belongs to the group: Get-ADGroupMembershipMetadata <group DN>." } elseif ($attribute -in $linkedAttributes) { Write-Warning "'$attribute' is a linked attribute: its history is carried by msDS-ReplValueMetaData, which this function does not read." } else { Write-Warning "No metadata for the attribute '$attribute' on $($adObject.DistinguishedName): the attribute is either not set or does not exist" } continue } $attributeMetadata } } else { $selectedMetadata = $replAttributeMetaData } foreach ($metadata in $selectedMetadata) { $object = [PSCustomObject][ordered]@{ ObjectDN = $adObject.DistinguishedName AttributeName = $metadata.pszAttributeName Version = [int]$metadata.dwVersion TimeLastOriginatingChange = ConvertTo-ADReplMetadataDate -Timestamp $metadata.ftimeLastOriginatingChange usnOriginatingChange = $metadata.usnOriginatingChange usnLocalChange = $metadata.usnLocalChange LastOriginatingDsaDN = $metadata.pszLastOriginatingDsaDN FromDomainController = $Server } $objectMetadata.Add($object) } $result = $objectMetadata if ($OnlyAttributesWithRecentChanges.IsPresent) { $limit = (Get-Date).ToUniversalTime().AddDays(-$Days) $result = $result | Where-Object { $null -ne $_.TimeLastOriginatingChange -and $_.TimeLastOriginatingChange -gt $limit } } if ($OnlyUpdatedAttributes.IsPresent) { $result = $result | Where-Object { $_.Version -gt 1 } } return $result } |