Private/InventoryHelpers.ps1
|
# Helpers for Get-NSPADVpnInventory. The pure ones are tested directly; the LDAP wrappers are thin # so tests can replace them. $script:ADInvEnrollGuid = [guid]'0e10c968-78fb-11d2-90d4-00c04f79dc55' $script:ADInvAutoEnrollGuid = [guid]'a05b8cc2-17bc-4802-a710-e7c15ab866a2' $script:ADInvClientAuthEku = '1.3.6.1.5.5.7.3.2' # Built-in v2 templates that carry Client Authentication but are never VPN user templates. $script:ADInvSkipTemplates = @('DomainControllerAuthentication', 'KerberosAuthentication', 'DirectoryEmailReplication') function ConvertFrom-ADTemplatePeriod { # pKIExpirationPeriod / pKIOverlapPeriod (8-byte little-endian, negative 100-ns ticks) -> whole days. param([byte[]]$Bytes) if (-not $Bytes -or $Bytes.Count -ne 8) { return $null } [int][math]::Round([math]::Abs([BitConverter]::ToInt64($Bytes, 0)) / 864000000000) } function Get-ADTameMyCertsOuStamp { # The OU= value a TameMyCerts policy XML forces into the subject (OutboundSubjectRule with Field # organizationalUnitName), or $null when the policy sets none. param([Parameter(Mandatory)][AllowEmptyString()][string]$XmlText) try { [xml]$policy = $XmlText } catch { return $null } foreach ($rule in @($policy.SelectNodes("//*[local-name()='OutboundSubjectRule']"))) { $field = $rule.SelectSingleNode("*[local-name()='Field']") $value = $rule.SelectSingleNode("*[local-name()='Value']") if ($field -and $value -and $field.InnerText.Trim() -eq 'organizationalUnitName') { return $value.InnerText.Trim() } } $null } function Get-ADNpsGroupSids { # Every SID named by a USERNTGROUPS(...) condition in ias.xml text. Reads the condition strings # only - never the RADIUS client entries (which hold shared secrets). param([Parameter(Mandatory)][AllowEmptyString()][string]$IasText) $sids = New-Object System.Collections.Generic.List[string] foreach ($condition in [regex]::Matches($IasText, 'USERNTGROUPS\(([^)]*)\)')) { foreach ($quoted in [regex]::Matches($condition.Groups[1].Value, '(?:"|")(S-1-[0-9-]+)(?:"|")')) { if (-not $sids.Contains($quoted.Groups[1].Value)) { $sids.Add($quoted.Groups[1].Value) } } } @($sids) } function Test-ADInvOrdinaryDomainSid { # True for a domain account SID with RID 1000+ (created by an admin); false for well-known and # built-in principals (Domain Admins 512, Domain Computers 515, RAS and IAS Servers 553, ...). param([AllowEmptyString()][string]$Sid) if ($Sid -notmatch '^S-1-5-21-\d+-\d+-\d+-(\d+)$') { return $false } [int64]$Matches[1] -ge 1000 } function Get-ADTemplateAccess { # Splits template ACEs into Enroll and AutoEnroll principal lists. GenericAll grants both; an # ExtendedRight ACE grants the right its ObjectType names (an empty ObjectType = all). param([AllowEmptyCollection()][object[]]$Rule = @()) $enroll = [ordered]@{} $autoEnroll = [ordered]@{} foreach ($ace in $Rule) { if (-not $ace.Allow) { continue } $all = $ace.Rights -match 'GenericAll' $extended = $ace.Rights -match 'ExtendedRight' $anyRight = $extended -and ([guid]$ace.ObjectType -eq [guid]::Empty) $principal = [pscustomobject]@{ Name = $ace.Name; Sid = $ace.Sid } if ($all -or $anyRight -or ($extended -and [guid]$ace.ObjectType -eq $script:ADInvEnrollGuid)) { $enroll[$ace.Sid] = $principal } if ($all -or $anyRight -or ($extended -and [guid]$ace.ObjectType -eq $script:ADInvAutoEnrollGuid)) { $autoEnroll[$ace.Sid] = $principal } } [pscustomobject]@{ Enroll = @($enroll.Values); AutoEnroll = @($autoEnroll.Values) } } function Get-ADInvNamingContext { $rootDse = [ADSI]'LDAP://RootDSE' $dnsDomain = try { [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain().Name } catch { $env:USERDNSDOMAIN } [pscustomobject]@{ Default = [string]$rootDse.defaultNamingContext[0] Configuration = [string]$rootDse.configurationNamingContext[0] DnsDomain = $dnsDomain } } function Search-ADInvDirectory { # Paged LDAP search returning one hashtable per result: property name (lower case) -> value array. param( [Parameter(Mandatory)][string]$SearchBase, [Parameter(Mandatory)][string]$Filter, [Parameter(Mandatory)][string[]]$Property ) # A '/' inside a DN (e.g. an OU named "Sales/Ops") must be escaped in an LDAP:// path. $searcher = New-Object System.DirectoryServices.DirectorySearcher([ADSI]"LDAP://$($SearchBase -replace '/', '\/')", $Filter) $searcher.PageSize = 500 foreach ($name in $Property) { [void]$searcher.PropertiesToLoad.Add($name) } $results = $searcher.FindAll() try { foreach ($result in $results) { $item = @{} foreach ($name in $result.Properties.PropertyNames) { $item[$name.ToLowerInvariant()] = @($result.Properties[$name]) } $item } } finally { $results.Dispose() } } function Get-ADInvAccessRule { # The explicit and inherited access rules on one AD object, as plain objects for Get-ADTemplateAccess. param([Parameter(Mandatory)][string]$DistinguishedName) $entry = [ADSI]"LDAP://$DistinguishedName" foreach ($rule in $entry.psbase.ObjectSecurity.GetAccessRules($true, $true, [System.Security.Principal.SecurityIdentifier])) { $sid = $rule.IdentityReference.Value $name = try { $rule.IdentityReference.Translate([System.Security.Principal.NTAccount]).Value } catch { $sid } @{ Sid = $sid; Name = $name; Allow = $rule.AccessControlType -eq 'Allow'; Rights = [string]$rule.ActiveDirectoryRights; ObjectType = $rule.ObjectType } } } function Get-ADInvTameMyCertsDirectory { # The TameMyCerts PolicyDirectory from this machine's CA registry, or $null. $root = 'HKLM:\SYSTEM\CurrentControlSet\Services\CertSvc\Configuration' if (-not (Test-Path $root)) { return $null } foreach ($ca in Get-ChildItem $root -ErrorAction SilentlyContinue) { $key = Join-Path $ca.PSPath 'PolicyModules\TameMyCerts.Policy' $value = try { (Get-ItemProperty -Path $key -Name PolicyDirectory -ErrorAction Stop).PolicyDirectory } catch { $null } if ($value) { return $value } } $null } function Get-NSPADModuleVersion { return [string]$ExecutionContext.SessionState.Module.Version } |