# DocumentADAttributes.ps1 # A PowerShell script to document all attributes of a specified Active Directory object. # # Copyright (c) 2019 Richard L. Mueller # Version 1.0 - January 30, 2019 # Version 2.0 - February 8, 2019 - Document multi-valued SID syntax operational attributes. # Version 2.1 - February 22, 2019 - Fixed typos. # # ---------------------------------------------------------------------- # You have a royalty-free right to use, modify, reproduce, and # distribute this script file in any way you find useful, provided that # you agree that the copyright owner above has no warranty, obligations, # or liability for such use. If ($Args.Count -eq 1) { # Retrieve the Active Directory object distinguished name from the command line. $ObjectDN = $Args[0] } ElseIf ($Args.Count -gt 1) { Write-Host "Only one command line parameter, an object distinguished name, is allowed." ` -ForegroundColor Red -BackgroundColor Black Write-Host "Script aborted." -ForegroundColor Red -BackgroundColor Black Break } Else { # Prompt for the Active Directory object distinguished name. $ObjectDN = Read-Host "Enter the AD object distinguished name" } # Bind to the Active Directory object specified. $ADObject = [ADSI]"LDAP://$ObjectDN" If (-Not $ADObject.Name) { # Object not found. Write-Host "Error: Active Directory object $ObjectDN not found." ` -ForegroundColor Red -BackgroundColor Black Write-Host "Script aborted." -ForegroundColor Red -BackgroundColor Black Break } # Determine the Naming Context for the object. $Domain = [System.DirectoryServices.DirectoryEntry]([ADSI]"LDAP://rootDSE") $NCs = $Domain.Get("NamingContexts") # The correct Naming Context will be the longest one that matches the specified DN. $Max = 0 ForEach ($NC In $NCs) { If ($ObjectDN -Like "*$NC") { $Length = $NC.Length If ($Length -gt $Max) { $Max = $Length $Base = $NC } } } # Search the naming context. $Searcher = New-Object System.DirectoryServices.DirectorySearcher $Searcher.SearchScope = "subtree" $Searcher.SearchRoot = "LDAP://$Base" # Retrieve local Time Zone bias from machine registry in hours. # This bias does not change with Daylight Savings Time. # This bias is used with the logonHours attribute. $LHBias = [Math]::Round((Get-ItemProperty ` -Path HKLM:\System\CurrentControlSet\Control\TimeZoneInformation).Bias/60, ` 0, [MidpointRounding]::AwayFromZero) # Retrieve local Time Zone bias from machine registry in hours. # This bias changes with Daylight Savings Time. # This bias is used with the schedule attribute. $SchedBias = (Get-ItemProperty -Path ` HKLM:\System\CurrentControlSet\Control\TimeZoneInformation).ActiveTimeBias # Account for negative bias. If ($SchedBias -gt 10080){$SchedBias = $SchedBias - 4294967296} $SchedBias = [Math]::Round($SchedBias/60, 0, [MidpointRounding]::AwayFromZero) Function OctetToGUID ($Octet) { # Function to convert Octet value (byte array) into string GUID value. $GUID = [GUID]$Octet Return $GUID.ToString("B") } Function HexToGUID ($Hex) { If ($Hex.Length -eq 32) { # Function to convert hex string into string GUID value. $GUID = "{" + $Hex.SubString(6,2) + $Hex.SubString(4,2) + $Hex.SubString(2,2) ` + $Hex.SubString(0,2) + "-" + $Hex.SubString(10,2) + $Hex.SubString(8,2) + "-" ` + $Hex.SubString(14,2) + $Hex.SubString(12,2) + "-" + $Hex.SubString(16,2) ` + $Hex.SubString(18,2) + "-" + $Hex.SubString(20,12) + "}" } Else {$GUID = "{" + $Hex + "}"} Return $GUID.ToLower() } # Create an array of 168 bytes, representing the hours in a week, # for the logonHours attribute. $LH = New-Object 'object[]' 168 Function OctetToHours ($Octet) { # Function to convert Octet value (byte array) into binary string # representing logonHours attribute. The 168 bits represent 24 hours # per day for 7 days, Sunday through Saturday. The values are converted # into local time. If the bit is "1", the user is allowed to logon # during that hour. If the bit is "0", the user is not allowed to logon. # Modified to display each day on a separate line. For ($j = 0; $j -le 20; $j = $j + 1) { For ($k = 7; $k -ge 0; $k = $k - 1) { $m = 8*$j + $k - $LHBias If ($m -lt 0) {$m = $m + 168} If ($Octet[$j] -band [Math]::Pow(2, $k)) {$LH[$m] = "1"} Else {$LH[$m] = "0"} } } $Hours = "" For ($j = 0; $j -le 6; $j = $j + 1) { For ($k = 0; $k -le 2; $k = $k + 1) { $n = 8*($j + $k) If ($k -eq 0) {$Hours = $Hours + [String]::Join("", $LH[$n..($n + 7)])} Else {$Hours = $Hours + "-" + [String]::Join("", $LH[$n..($n + 7)])} If (($k -eq 2) -And ($j -ne 6)) {$Hours = $Hours + "`r`n "} } } Return $Hours } Function OctetToSchedule ($Sched) { # Function to document the schedule attribute of a connection object. # Ignore first 20 bytes of schedule array (index 0 through 19). # This is a header line that is always the same. $Start = 20 # Loop through the last 168 bytes, from index 20 to 187. These bytes # represent the 168 hours in a week, Sunday through Saturday, in UTC. # Loop first through each day of the week (index 0 through 6). $Output = "" For ($j = 0; $j -le 6; $j = $j + 1) { # Line to be output for this day of the week. If ($j -eq 0) {$Line = ""} Else {$Line = "`r`n "} $L = "" $First = $True # Loop through the 24 hours in the day (index 0 through 23). For ($k = 0; $k -le 23; $k = $k + 1) { # Adjust for time zone bias. The values in the schedule attribute are # in UTC, so they must be adjusted for the local time zone bias. # $m is the index into the byte array $Sched. $m = $start + $k + $SchedBias # If the time zone adjusted index is less than 20, add 168. If ($m -lt 20) {$m = $m + 168} # If the time zone adjusted index is greater than 187, subtract 168. If ($m -gt 187) {$m = $m - 168} # Each byte indicates when replication is scheduled in the hour. # Default output indicating which 15 minute interval # in each hour when replication scheduled. Switch ($Sched[$m]) { 0 {$X = "nnnn"} 1 {$X = "Ynnn"} 2 {$X = "nYnn"} 3 {$X = "YYnn"} 4 {$X = "nnYn"} 5 {$X = "YnYn"} 6 {$X = "nYYn"} 7 {$X = "YYYn"} 8 {$X = "nnnY"} 9 {$X = "YnnY"} 10 {$X = "nYnY"} 11 {$X = "YYnY"} 12 {$X = "nnYY"} 13 {$X = "YnYY"} 14 {$X = "nYYY"} 15 {$X = "YYYY"} } # End Switch statement for default output. # Construct the line for this day. $Line = "$Line$L$X" If ($First) { $First = $False $L = " " } } # End of loop for the 24 hours in a day. # Output for this day. $Output = "$Output$Line" # Advance 24 hours (bytes) to the next day. $Start = $Start + 24 } # End of loop for the 7 days in a week. Return $Output } Function UAC ($Flag) { # Function to evaluate the userAccountControl # and msDS-User-Account-Control-Computed attributes. $Setting = "" If ($Flag -band 0x02) {$Setting = $Setting + "AccountDisabled "} If ($Flag -band 0x08) {$Setting = $Setting + "HomeDirReqd "} If ($Flag -band 0x10) {$Setting = $Setting + "LockedOut "} If ($Flag -band 0x20) {$Setting = $Setting + "PwdNotReqd "} If ($Flag -band 0x40) {$Setting = $Setting + "PwdCannotChg "} If ($Flag -band 0x80) {$Setting = $Setting + "EncryptedTextPwdAllowed "} If ($Flag -band 0x100) {$Setting = $Setting + "TempDuplAccount "} If ($Flag -band 0x200) {$Setting = $Setting + "NormalAccount "} If ($Flag -band 0x800) {$Setting = $Setting + "InterdomnainTrustAcct "} If ($Flag -band 0x1000) {$Setting = $Setting + "WorkstationTrustAcct "} If ($Flag -band 0x2000) {$Setting = $Setting + "ServerTrustAcct "} If ($Flag -band 0x10000) {$Setting = $Setting + "PwdDoesNotExpire "} If ($Flag -band 0x20000) {$Setting = $Setting + "MNSLogonAcct "} If ($Flag -band 0x40000) {$Setting = $Setting + "SmartcardReqd "} If ($Flag -band 0x80000) {$Setting = $Setting + "TrustedForDelgation "} If ($Flag -band 0x100000) {$Setting = $Setting + "NotDelegated "} If ($Flag -band 0x200000) {$Setting = $Setting + "UseDESKeyOnly "} If ($Flag -band 0x400000) {$Setting = $Setting + "RequirePreauth "} If ($Flag -band 0x800000) {$Setting = $Setting + "PwdExpired "} If ($Flag -band 0x1000000) {$Setting = $Setting + "TrustedToAuthForDelegation "} If ($Flag -band 0x4000000) {$Setting = $Setting + "PartialSecretsAcct "} If ($Flag -band 0x8000000) {$Setting = $Setting + "UseAESKeysOnly "} Return " (" + $Setting.Trim() + ")" } Function GroupType ($Flag) { $GT = "" # Function to retrieve group type from the groupType attribute. If ($Flag -band 0x01) {$GT = $GT + "Built-in "} If ($Flag -band 0x02) {$GT = $GT + "Global "} If ($Flag -band 0x04) {$GT = $GT + "Local "} If ($Flag -band 0x08) {$GT = $GT + "Universal "} If ($Flag -band 0x10) {$GT = $GT + "APP_BASIC "} If ($Flag -band 0x20) {$GT = $GT + "APP_QUERY "} If ($Flag -band 0x80000000) {$GT = $GT.Trim() + "/Security"} Else {$GT = $GT.Trim() + "/Distribution"} Return " ($GT)" } Function SearchFlags ($Flag) { $SF= "" # Function to evaluate the searchFlags attribute. If ($Flag -band 0x01) {$SF = $SF + "Indexed "} If ($Flag -band 0x02) {$SF = $SF + "IndexedEachContainer "} If ($Flag -band 0x04) {$SF = $SF + "InANRSet "} If ($Flag -band 0x08) {$SF = $SF + "PreservedInTombstone "} If ($Flag -band 0x10) {$SF = $SF + "CopiedWhenObjectCopied "} If ($Flag -band 0x20) {$SF = $SF + "TupleIndex "} If ($Flag -band 0x40) {$SF = $SF + "VLVIndex "} Return " (" + $SF.Trim() + ")" } Function SystemFlags ($Flag) { $SysF = "" # Function to evaluate the systemFlags attribute. If ($Flag -band 0x01) {$SysF = $SysF + "AttrReplicated/NTDSCrossRefObj "} If ($Flag -band 0x02) {$SysF = $SysF + "ReplToGC/DomainCrossRefObj "} If ($Flag -band 0x04) {$SysF = $SysF + "AttrConstructed "} If ($Flag -band 0x10) {$SysF = $SysF + "AttrInBaseSchema "} If ($Flag -band 0x02000000) {$SysF = $SysF + "DelImmediately "} If ($Flag -band 0x04000000) {$SysF = $SysF + "CannotBeMoved "} If ($Flag -band 0x08000000) {$SysF = $SysF + "CannotBeRenamed "} If ($Flag -band 0x10000000) {$SysF = $SysF + "CanBeMovedWithRestrictions "} If ($Flag -band 0x20000000) {$SysF = $SysF + "CanBeMoved "} If ($Flag -band 0x40000000) {$SysF = $SysF + "CanBeRenamed "} If ($Flag -band 0x80000000) {$SysF = $SysF + "CannotBeDeleted "} Return " (" + $SysF.Trim() + ")" } Function SAMType ($Flag) { # Function to evaluate the sAMAccountType attribute. Switch ($Flag) { 0x10000000 {$ST = "GroupObject"} 0x10000001 {$ST = "NonSecurityGroupObject"} 0x20000000 {$ST = "AliasObject"} 0x20000001 {$ST = "NonSecurityAliasObject"} 0x30000000 {$ST = "UserAccount"} 0x30000001 {$ST = "MachineAccount"} 0x30000002 {$ST = "TrustAccount"} 0x40000000 {$ST = "AppBasicAccount"} 0x40000001 {$ST = "AppQueryAccount"} } Return " ($ST)" } Function InstanceType ($Flag) { $IT= "" # Function to evaluate the searchFlags attribute. If ($Flag -band 0x01) {$IT = $IT + "NCHead "} If ($Flag -band 0x02) {$IT = $IT + "ReplicaNotInstantiated "} If ($Flag -band 0x04) {$IT = $IT + "Writeable "} If ($Flag -band 0x08) {$IT = $IT + "NCAboveHeld "} If ($Flag -band 0x10) {$IT = $IT + "NCBeingConstructed "} If ($Flag -band 0x20) {$IT = $IT + "NCBeingRemoved "} Return " (" + $IT.Trim() + ")" } # Determine the object class. $Schema = [DirectoryServices.ActiveDirectory.ActiveDirectorySchema]::GetCurrentSchema() $Class = $ADObject.objectClass.ToString().Split(" ")[-1] "Object DN: $ObjectDN" "Object class: $Class" # Filter on the object. $Searcher.Filter = "(distinguishedName=$ObjectDN)" # Retrieve attributes for this class from the Schema. $ManAttributes = $Schema.FindClass("$Class").MandatoryProperties | Select Name, Syntax, IsSingleValued, CommonName $OptAttributes = $Schema.FindClass("$Class").OptionalProperties | Select Name, Syntax, IsSingleValued, CommonName $ManCount = $ManAttributes.Count $OptCount = $OptAttributes.Count ForEach ($Attribute In $ManAttributes) { # It is assumed that no mandatory attributes are multi-valued Sid syntax and operational. $AttrName = $Attribute.Name $Searcher.PropertiesToLoad.Add($AttrName) > $Null } $Result = $Searcher.FindOne() "Mandatory Attributes ($ManCount)" ForEach ($Attribute In $ManAttributes) { $AttrName = $Attribute.Name $AttrSyntax = $Attribute.Syntax $Single = $Attribute.IsSingleValued If (-Not $Single) {$AttrSyntax = "$AttrSyntax[]"} $Name = $Attribute.CommonName # Retrieve systemFlags attribute of the attribute. $Attr = [ADSI]"LDAP://cn=$Name,$Schema" $Flags = $Attr.psbase.Properties.Item("systemFlags")[0] If ($Flags -band 4) {$AttrSyntax = "$AttrSyntax "} $Values = $Result.Properties.Item($AttrName) If ($Values[0] -eq $Null) { " $AttrName ($AttrSyntax): " } Else { ForEach ($Value In $Values) { Switch ($Value.GetType().Name) { "Int64" { If (($Value -ge [TimeSpan]::MaxValue.Ticks) ` -Or ($Value -le [TimeSpan]::MinValue.Ticks)) { " $AttrName ($AttrSyntax): " } Else { If (($Value -gt 120000000000000000) ` -And ($Value -le [DateTime]::MaxValue.Ticks)) { $Date = [DateTime]$Value " $AttrName ($AttrSyntax): " + '{0:n0}' -f $Value ` + " (" + $Date.AddYears(1600).ToLocalTime() + ")" } Else { If ($Value -lt 0) { $Span = [TimeSpan](-$Value) " $AttrName ($AttrSyntax): " + '{0:n0}' -f $Value ` + " ($Span [Days.Hours:Minutes:Seconds])" } Else {" $AttrName ($AttrSyntax): " + '{0:n0}' -f $Value} } } } "Byte[]" { If (($Value.Length -eq 16) ` -And ($AttrName.ToUpper().Contains("GUID") -eq $True)) { " $AttrName ($AttrSyntax): " + $(OctetToGUID($Value)) } Else { If (($Value.Length -eq 21) -And ($AttrName -eq "logonHours")) { " $AttrName ($AttrSyntax): " + $(OctetToHours($Value)) } Else { If (($Value[0] -eq 1) -And (` (($Value[1] -eq 1) -And ($Value.Length -eq 12)) ` -Or (($Value[1] -eq 2) -And ($Value.Length -eq 16)) ` -Or (($Value[1] -eq 4) -And ($Value.Length -eq 24)) ` -Or (($Value[1] -eq 5) -And ($Value.Length -eq 28)))) { $SID = New-Object System.Security.Principal.SecurityIdentifier $Value, 0 " $AttrName ($AttrSyntax): $SID" } Else { If ($AttrName -eq "schedule") { " $AttrName ($AttrSyntax): " + $(OctetToSchedule $Value) } Else { $Size = $Value.Count If ($Size -gt 100) { " $AttrName ($AttrSyntax) <$Size bytes>" } Else { " $AttrName ($AttrSyntax): $Value" } } } } } } "Int32" { Switch ($AttrName.ToLower()) { "useraccountcontrol" { " $AttrName ($AttrSyntax): $Value " + $(UAC($Value)) } "msds-user-account-control-computed" { " $AttrName ($AttrSyntax): $Value " + $(UAC($Value)) } "grouptype" { " $AttrName ($AttrSyntax): $Value " + $(GroupType($Value)) } "searchflags" { " $AttrName ($AttrSyntax): $Value " + $(SearchFlags($Value)) } "systemflags" { " $AttrName ($AttrSyntax): $Value " + $(SystemFlags($Value)) } "samaccounttype" { " $AttrName ($AttrSyntax): $Value " + $(SAMType($Value)) } "instancetype" { " $AttrName ($AttrSyntax): $Value " + $(InstanceType($Value)) } Default { " $AttrName ($AttrSyntax): " + '{0:n0}' -f $Value } } } "Boolean" { If ($Value -eq 0) {$Boolean = "TRUE"} Else {$Boolean = "FALSE"} " $AttrName ($AttrSyntax): $Boolean" } "String" { If ($AttrSyntax -Like "DNWithBinary*") { $Items = $Value.Split(":") $DN = $Items[3] $GUID = HexToGUID $Items[2] " $AttrName ($AttrSyntax): $DN $GUID" } Else { " $AttrName ($AttrSyntax): $Value" } } Default { " $AttrName ($AttrSyntax): $Value" } } # End Switch ($Value.GetType().Name). } # End ForEach ($Value In $Values). } # End $Values not Null and not operational. } # End ForEach $ManAttributes. $Searcher.Dispose() $Searcher = New-Object System.DirectoryServices.DirectorySearcher $Searcher.SearchScope = "subtree" $Searcher.SearchRoot = "LDAP://$Base" # Filter on the object. $Searcher.Filter = "(distinguishedName=$ObjectDN)" "Optional Attributes ($OptCount)" ForEach ($Attribute In $OptAttributes) { $AttrName = $Attribute.Name $AttrSyntax = $Attribute.Syntax $Single = $Attribute.IsSingleValued If (-Not $Single) {$AttrSyntax = "$AttrSyntax[]"} $Name = $Attribute.CommonName # Retrieve systemFlags attribute of the attribute. $Attr = [ADSI]"LDAP://cn=$Name,$Schema" $Flags = $Attr.psbase.Properties.Item("systemFlags")[0] If ($Flags -band 4) {$AttrSyntax = "$AttrSyntax "} # A error is raised if we attempt to retrieve multi-valued Sid operational attributes. If ($AttrSyntax -ne "Sid[] ") { $Searcher.PropertiesToLoad.Add($AttrName) > $Null } $Result = $Searcher.FindOne() $Values = $Result.Properties.Item($AttrName) If ($AttrSyntax -eq "Sid[] ") { $ADObject.psbase.RefreshCache("$AttrName") $SIDs = $ADObject.psbase.Properties.Item("$AttrName") ForEach ($Value In $SIDs) { $SID = New-Object System.Security.Principal.SecurityIdentifier $Value, 0 $Group = $SID.Translate([System.Security.Principal.NTAccount]) $GrpNTName = $Group.Value.Split("\")[1] " $AttrName ($AttrSyntax): $SID ($GrpNTName)" } } ElseIf ($Values[0] -eq $Null) { " $AttrName ($AttrSyntax): " } Else { ForEach ($Value In $Values) { Switch ($Value.GetType().Name) { "Int64" { If (($Value -ge [TimeSpan]::MaxValue.Ticks) ` -Or ($Value -le [TimeSpan]::MinValue.Ticks)) { " $AttrName ($AttrSyntax): " } Else { If (($Value -gt 120000000000000000) ` -And ($Value -le [DateTime]::MaxValue.Ticks)) { $Date = [DateTime]$Value " $AttrName ($AttrSyntax): " + '{0:n0}' -f $Value ` + " (" + $Date.AddYears(1600).ToLocalTime() + ")" } Else { If ($Value -lt 0) { $Span = [TimeSpan](-$Value) " $AttrName ($AttrSyntax): " + '{0:n0}' -f $Value ` + " ($Span [Days.Hours:Minutes:Seconds])" } Else {" $AttrName ($AttrSyntax): " + '{0:n0}' -f $Value} } } } "Byte[]" { If (($Value.Length -eq 16) ` -And ($AttrName.ToUpper().Contains("GUID") -eq $True)) { " $AttrName ($AttrSyntax): " + $(OctetToGUID($Value)) } Else { If (($Value.Length -eq 21) -And ($AttrName -eq "logonHours")) { " $AttrName ($AttrSyntax): " + $(OctetToHours($Value)) } Else { If (($Value[0] -eq 1) -And (` (($Value[1] -eq 1) -And ($Value.Length -eq 12)) ` -Or (($Value[1] -eq 2) -And ($Value.Length -eq 16)) ` -Or (($Value[1] -eq 4) -And ($Value.Length -eq 24)) ` -Or (($Value[1] -eq 5) -And ($Value.Length -eq 28)))) { $SID = New-Object System.Security.Principal.SecurityIdentifier $Value, 0 " $AttrName ($AttrSyntax): $SID" } Else { If ($AttrName -eq "schedule") { " $AttrName ($AttrSyntax): " + $(OctetToSchedule $Value) } Else { $Size = $Value.Count If ($Size -gt 100) { " $AttrName ($AttrSyntax) <$Size bytes>" } Else { " $AttrName ($AttrSyntax): $Value" } } } } } } "Int32" { Switch ($AttrName.ToLower()) { "useraccountcontrol" { " $AttrName ($AttrSyntax): $Value " + $(UAC($Value)) } "msds-user-account-control-computed" { " $AttrName ($AttrSyntax): $Value " + $(UAC($Value)) } "grouptype" { " $AttrName ($AttrSyntax): $Value " + $(GroupType($Value)) } "searchflags" { " $AttrName ($AttrSyntax): $Value " + $(SearchFlags($Value)) } "systemflags" { " $AttrName ($AttrSyntax): $Value " + $(SystemFlags($Value)) } "samaccounttype" { " $AttrName ($AttrSyntax): $Value " + $(SAMType($Value)) } "instancetype" { " $AttrName ($AttrSyntax): $Value " + $(InstanceType($Value)) } Default { " $AttrName ($AttrSyntax): " + '{0:n0}' -f $Value } } } "Boolean" { If ($Value -eq 0) {$Boolean = "TRUE"} Else {$Boolean = "FALSE"} " $AttrName ($AttrSyntax): $Boolean" } "String" { If ($AttrSyntax -Like "DNWithBinary*") { $Items = $Value.Split(":") $DN = $Items[3] $GUID = HexToGUID $Items[2] " $AttrName ($AttrSyntax): $DN $GUID" } Else { " $AttrName ($AttrSyntax): $Value" } } Default { " $AttrName ($AttrSyntax): $Value" } } # End Switch ($Value.GetType().Name). } # End ForEach ($Value In $Values). } # End $Values not Null and not operational. } # End ForEach $OptAttributes.