Public/Schema/Get-ADSchemaAttribute.ps1
|
<#
.SYNOPSIS Report on the Active Directory schema attributes, with their indexing, replication and confidentiality flags. .DESCRIPTION Returns one object per attributeSchema object of the forest schema, with its definition (syntax, single valued, range, dates) and its 'searchFlags' decoded into readable columns rather than left as a raw integer. Two columns answer the confidentiality question, and they are not the same question: - 'Confidential' says the attribute IS marked confidential, which is bit 128 of 'searchFlags'. Reading it then requires the CONTROL_ACCESS right, a plain READ_PROPERTY is no longer enough. - 'CanBeConfidential' says the attribute COULD be marked confidential. Only attributes that are not part of the base schema can. Microsoft is explicit about it: bit 128 is "ignored for base schema attributes (systemFlags=0x10)". So an attribute can carry the bit and have it do nothing, which is exactly the kind of false sense of security an audit has to surface. That case shows up as Confidential = True and CanBeConfidential = False. Reference: https://learn.microsoft.com/troubleshoot/windows-server/windows-security/mark-attribute-as-confidential 'IsMemberOfPartialAttributeSet' tells whether the attribute is replicated to the global catalog, and 'RodcFiltered' whether it is deliberately kept away from read only domain controllers. .PARAMETER Attribute Restrict the output to the attributes whose LDAP display name matches. Wildcards are supported, so 'ms-Mcs-Adm*' or '*password*' work. Defaults to every attribute of the schema. .PARAMETER ConfidentialOnly Return only the attributes marked confidential. Shorthand for the audit question "what is confidential here". .PARAMETER Server Domain controller or domain to query. Defaults to the one selected by the ActiveDirectory module. .EXAMPLE Get-ADSchemaAttribute Returns every attribute of the schema with its decoded flags. .EXAMPLE Get-ADSchemaAttribute -Attribute 'msLAPS-Password' Returns the definition of the Windows LAPS password attribute. Its searchFlags of 904 decodes to RodcFiltered, NeverAudit, Confidential and PreserveOnDelete, which is the reference layout for a sensitive attribute. .EXAMPLE Get-ADSchemaAttribute -ConfidentialOnly Returns the attributes marked confidential in this forest. .EXAMPLE Get-ADSchemaAttribute -ConfidentialOnly | Where-Object { -not $_.CanBeConfidential } Returns the attributes carrying the confidential bit while being part of the base schema, so the bit is ignored and the data is readable by anyone. Worth checking on every audit. .EXAMPLE Get-ADSchemaAttribute | Where-Object { $_.CanBeConfidential -and -not $_.Confidential -and $_.LdapDisplayName -like '*password*' } Returns the extended attributes that look credential related and could be marked confidential but are not. .EXAMPLE Get-ADSchemaAttribute | Where-Object { $_.Confidential -and -not $_.RodcFiltered } Returns the confidential attributes that are still replicated to the read only domain controllers. Microsoft recommends doing both, so this is a gap worth reviewing. .EXAMPLE Get-ADSchemaAttribute | Export-Csv -Path 'C:\temp\ADSchemaInfo.csv' -NoTypeInformation -Encoding UTF8 -Delimiter ';' Exports the full schema report. .OUTPUTS System.Management.Automation.PSCustomObject, one per attributeSchema object. .NOTES Version : 2.0 - August 2026. Migrated from the ActiveDirectory-Toolbox repository. Author : Bastien Perez - ITPro-Tips (https://itpro-tips.com) .LINK https://itpro-tips.com #> function Get-ADSchemaAttribute { [CmdletBinding()] [OutputType([PSCustomObject])] param ( [Parameter(Position = 0)] [string]$Attribute, [switch]$ConfidentialOnly, [string]$Server ) $ErrorActionPreference = 'Stop' # Common parameters forwarded to every ActiveDirectory cmdlet call $adParameters = @{ ErrorAction = 'Stop' } if ($PSBoundParameters.ContainsKey('Server')) { $adParameters.Add('Server', $Server) } try { $rootDSE = Get-ADRootDSE @adParameters } catch { Write-Error "Unable to read the RootDSE: $($_.Exception.Message)" return } $schemaNamingContext = $rootDSE.schemaNamingContext # Filtering server side rather than in PowerShell, the schema partition holds thousands of objects if ([string]::IsNullOrWhiteSpace($Attribute)) { $filter = "objectClass -eq 'attributeSchema'" } else { $filter = "objectClass -eq 'attributeSchema' -and lDAPDisplayName -like '$Attribute'" } $properties = @( 'lDAPDisplayName', 'adminDescription', 'adminDisplayName', 'attributeID', 'attributeSyntax', 'isSingleValued', 'systemOnly', 'linkID', 'rangeLower', 'rangeUpper', 'searchFlags', 'systemFlags', 'isMemberOfPartialAttributeSet', 'schemaIDGUID', 'whenCreated', 'whenChanged' ) $getADObjectParams = $adParameters.Clone() $getADObjectParams.SearchBase = $schemaNamingContext $getADObjectParams.Filter = $filter $getADObjectParams.Properties = $properties [System.Collections.Generic.List[PSCustomObject]]$schemaAttributes = @() try { $attributesFound = @(Get-ADObject @getADObjectParams) } catch { Write-Error "Unable to query the schema partition: $($_.Exception.Message)" return } if ($attributesFound.Count -eq 0) { Write-Warning "No attribute matched '$Attribute' in the schema" return } Write-Verbose "[i] $($attributesFound.Count) attributes read from $schemaNamingContext" foreach ($attributeFound in $attributesFound) { $flags = ConvertFrom-ADSearchFlags -SearchFlags $attributeFound.searchFlags if ($ConfidentialOnly -and (-not $flags.Confidential)) { continue } # systemFlags bit 0x10 is FLAG_SCHEMA_BASE_OBJECT. The confidential bit is ignored on those attributes, # so they cannot be protected this way whatever searchFlags says. $systemFlags = 0 if ($null -ne $attributeFound.systemFlags) { $systemFlags = [int]$attributeFound.systemFlags } $isBaseSchemaObject = [bool]($systemFlags -band 0x10) $schemaAttributes.Add([PSCustomObject][ordered]@{ LdapDisplayName = $attributeFound.lDAPDisplayName AdminDisplayName = $attributeFound.adminDisplayName AdminDescription = $attributeFound.adminDescription AttributeID = $attributeFound.attributeID AttributeSyntax = $attributeFound.attributeSyntax IsSingleValued = $attributeFound.isSingleValued SystemOnly = $attributeFound.systemOnly LinkID = $attributeFound.linkID RangeLower = $attributeFound.rangeLower RangeUpper = $attributeFound.rangeUpper IsMemberOfPartialAttributeSet = [bool]$attributeFound.isMemberOfPartialAttributeSet Confidential = $flags.Confidential CanBeConfidential = (-not $isBaseSchemaObject) IsBaseSchemaObject = $isBaseSchemaObject RodcFiltered = $flags.RodcFiltered NeverAudit = $flags.NeverAudit Indexed = $flags.Indexed ContainerIndexed = $flags.ContainerIndexed ANR = $flags.ANR PreserveOnDelete = $flags.PreserveOnDelete CopyOnCopy = $flags.CopyOnCopy TupleIndexed = $flags.TupleIndexed SubtreeIndexed = $flags.SubtreeIndexed SearchFlags = $flags.SearchFlags SystemFlags = $systemFlags DistinguishedName = $attributeFound.DistinguishedName ObjectGUID = $attributeFound.ObjectGUID WhenCreated = $attributeFound.whenCreated WhenChanged = $attributeFound.whenChanged }) } return $schemaAttributes } |