Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
## AD helper restoration (unreleased)

- Restore Test-IsValidDn, Test-IsValidUpn and Get-ReportChain; export twenty-seven commands.
- Replace DN/UPN regexes with documented practical syntax policies and pipeline support.
- Support DN escapes, multi-valued RDNs, long UPN suffixes and IDN suffixes.
- Preserve report-chain identities, aliases, server selection and property projection.
- Escape LDAP assertion values for UPN lookup and transitive manager queries.
- Require exactly one manager, exclude the manager from results and propagate AD errors.
- Keep ActiveDirectory optional at import; add mocked query and syntax regression tests.
- Complete migration of all former Staging/v3 candidates.

## Filesystem naming and registry restoration (unreleased)

- Restore Remove-SpecialCharacters and Test-RegistryValue; export twenty-four commands.
Expand Down
3 changes: 3 additions & 0 deletions IT-ToolBox.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
'Get-OsUpTime'
'Remove-SpecialCharacters'
'Test-RegistryValue'
'Test-IsValidDn'
'Test-IsValidUpn'
'Get-ReportChain'
'New-StringEncryption'
'New-StringDecryption'
'New-RandomString'
Expand Down
3 changes: 3 additions & 0 deletions IT-ToolBox.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ Export-ModuleMember -Function @(
'Get-OsUpTime'
'Remove-SpecialCharacters'
'Test-RegistryValue'
'Test-IsValidDn'
'Test-IsValidUpn'
'Get-ReportChain'
'New-StringEncryption'
'New-StringDecryption'
'New-RandomString'
Expand Down
11 changes: 11 additions & 0 deletions Private/ConvertTo-ITToolBoxLdapFilterValue.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
function ConvertTo-ITToolBoxLdapFilterValue {
param([string]$Value)
$builder = [System.Text.StringBuilder]::new()
foreach ($byte in [System.Text.UTF8Encoding]::new($false, $true).GetBytes($Value)) {
if ($byte -in @(0, 40, 41, 42, 92) -or $byte -ge 128) {
[void]$builder.Append('\').Append($byte.ToString('x2'))
}
else { [void]$builder.Append([char]$byte) }
}
return $builder.ToString()
}
7 changes: 7 additions & 0 deletions Private/Invoke-ITToolBoxAdUser.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
function Invoke-ITToolBoxAdUser {
param([hashtable]$Query)
if (-not (Get-Command Get-ADUser -ErrorAction SilentlyContinue)) {
throw [InvalidOperationException]::new('Get-ReportChain requires Get-ADUser from the ActiveDirectory module and a reachable AD endpoint.')
}
Get-ADUser @Query
}
38 changes: 38 additions & 0 deletions Private/Test-ITToolBoxDnSyntax.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
function Test-ITToolBoxDnSyntax {
param([string]$Value)
if ([string]::IsNullOrWhiteSpace($Value) -or $Value -match '\p{Cc}') { return $false }
$parts = [System.Collections.Generic.List[string]]::new()
$start = 0
for ($i = 0; $i -lt $Value.Length; $i++) {
if ($Value[$i] -eq '\') { $i++; continue }
if ($Value[$i] -in @(',', '+')) {
$parts.Add($Value.Substring($start, $i - $start))
$start = $i + 1
}
}
$parts.Add($Value.Substring($start))
foreach ($part in $parts) {
if ($part -cnotmatch '^(?:[a-zA-Z][a-zA-Z0-9-]*|[0-9]+(?:\.[0-9]+)+)=(.*)$') { return $false }
$text = $Matches[1]
if ($text.Length -eq 0) { return $false }
if ($text.StartsWith('#')) {
# Validate hex-string notation only; do not claim to validate ASN.1/BER contents.
if ($text -cnotmatch '^#(?:[a-fA-F0-9]{2})+$') { return $false }
continue
}
for ($j = 0; $j -lt $text.Length; $j++) {
$ch = $text[$j]
if ($ch -eq '\') {
if ($j + 1 -ge $text.Length) { return $false }
if ($j + 2 -lt $text.Length -and $text.Substring($j + 1, 2) -cmatch '^[a-fA-F0-9]{2}$') {
$j += 2
}
elseif ($text[$j + 1] -in @(' ', '"', '#', '+', ',', ';', '<', '=', '>', '\')) { $j++ }
else { return $false }
}
elseif ($ch -in @('"', '+', ',', ';', '<', '>') -or
($ch -eq ' ' -and ($j -eq 0 -or $j -eq $text.Length - 1))) { return $false }
}
}
return $true
}
64 changes: 64 additions & 0 deletions Public/Get-ReportChain.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
function Get-ReportChain {
<#
.SYNOPSIS
Returns users reporting directly or transitively to an AD manager.
.DESCRIPTION
Resolves exactly one manager and queries AD's matching-rule-in-chain manager
relationship. Excludes the manager from results. Escapes LDAP assertion values,
applies DomainController to both queries and propagates errors. Requires the
optional ActiveDirectory Get-ADUser command at invocation, not module import.
Results are selected in Properties order; * returns all requested properties.
#>
[CmdletBinding(DefaultParameterSetName = 'DistinguishedName')]
[OutputType([pscustomobject])]
param(
[Parameter(ParameterSetName = 'SamAccountName', Mandatory = $true)]
[ValidateNotNullOrEmpty()][Alias('UserSam', 'SAM')]
[string]$SamAccountName,
[Parameter(ParameterSetName = 'UserPrincipalName', Mandatory = $true)]
[ValidateNotNullOrEmpty()][Alias('UPN', 'UserUPN')]
[string]$UserPrincipalName,
[Parameter(ParameterSetName = 'DistinguishedName', Mandatory = $true)]
[ValidateNotNullOrEmpty()][Alias('DN', 'DistinguishedName')]
[string]$UserDN,
[ValidateNotNullOrEmpty()]
[string]$DomainController,
[ValidateNotNullOrEmpty()]
[string[]]$Properties = @('SamAccountName', 'UserPrincipalName', 'Mail', 'Manager', 'DirectReports')
)
foreach ($property in $Properties) {
if ([string]::IsNullOrWhiteSpace($property)) { throw 'Properties must contain nonempty property names.' }
}
if ($PSBoundParameters.ContainsKey('DomainController') -and [string]::IsNullOrWhiteSpace($DomainController)) {
throw 'DomainController cannot be whitespace.'
}
$lookup = @{ Properties = $Properties; ErrorAction = 'Stop' }
switch ($PSCmdlet.ParameterSetName) {
'SamAccountName' {
if ([string]::IsNullOrWhiteSpace($SamAccountName)) { throw 'SamAccountName cannot be whitespace.' }
$lookup.Identity = $SamAccountName
}
'UserPrincipalName' {
if (-not (Test-IsValidUpn $UserPrincipalName)) { throw 'UserPrincipalName does not match the supported UPN syntax policy.' }
$escapedUpn = ConvertTo-ITToolBoxLdapFilterValue $UserPrincipalName
$lookup.LDAPFilter = '(userPrincipalName={0})' -f $escapedUpn
}
'DistinguishedName' {
if (-not (Test-IsValidDn $UserDN)) { throw 'UserDN does not match the supported DN syntax policy.' }
$lookup.Identity = $UserDN
}
}
if ($DomainController) { $lookup.Server = $DomainController }
$managers = @(Invoke-ITToolBoxAdUser -Query $lookup)
if ($managers.Count -ne 1) { throw 'Manager lookup must resolve exactly one user.' }
$managerDn = [string]$managers[0].DistinguishedName
if (-not (Test-IsValidDn $managerDn)) { throw 'The resolved manager did not return a supported distinguished name.' }
$escapedDn = ConvertTo-ITToolBoxLdapFilterValue $managerDn
$query = @{
Properties = $Properties
LDAPFilter = '(&(manager:1.2.840.113556.1.4.1941:={0})(!(distinguishedName={0})))' -f $escapedDn
ErrorAction = 'Stop'
}
if ($DomainController) { $query.Server = $DomainController }
Invoke-ITToolBoxAdUser -Query $query | Select-Object -Property $Properties
}
22 changes: 22 additions & 0 deletions Public/Test-IsValidDn.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
function Test-IsValidDn {
<#
.SYNOPSIS
Tests a practical nonempty distinguished-name string syntax.
.DESCRIPTION
Supports attribute descriptors/OIDs, escaped separators, hex escapes and
multi-valued RDNs. Does not require CN/OU/DC attributes or a DC suffix. Rejects
empty attribute values, literal controls, dangling/invalid escapes, unescaped
edge spaces and legacy quoted values. Hex-string notation is checked without
validating BER contents. No schema, directory existence, canonical equality
or decoded UTF-8 validation is performed; this is not a complete RFC parser.
#>
[CmdletBinding()]
[OutputType([bool])]
param(
[Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
[AllowNull()][AllowEmptyString()]
[Alias('DN', 'DistinguishedName')]
[string]$ObjectDN
)
process { return (Test-ITToolBoxDnSyntax -Value $ObjectDN) }
}
27 changes: 27 additions & 0 deletions Public/Test-IsValidUpn.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
function Test-IsValidUpn {
<#
.SYNOPSIS
Tests a practical UPN syntax policy without directory access.
.DESCRIPTION
Requires one @ separator. The ASCII username starts/ends with a letter or
digit and may contain dots, underscores, hyphens and apostrophes internally;
consecutive dots are rejected. The suffix is a DNS/IDN name, including a
single-label name or long suffix. No email parsing, account existence, suffix
registration or complete AD/Entra account-creation policy is implied.
#>
[CmdletBinding()]
[OutputType([bool])]
param(
[Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
[AllowNull()][AllowEmptyString()]
[Alias('UPN', 'ADUpn', 'UniversalPrincipalName')]
[string]$UserUpn
)
process {
if ([string]::IsNullOrWhiteSpace($UserUpn) -or $UserUpn -match '[\s\p{Cc}]') { return $false }
$parts = $UserUpn.Split('@')
if ($parts.Count -ne 2 -or $parts[0].Contains('..') -or
$parts[0] -cnotmatch '^[a-zA-Z0-9](?:[a-zA-Z0-9._''-]*[a-zA-Z0-9])?$') { return $false }
return (Test-ITToolBoxDnsName -Name $parts[1])
}
}
55 changes: 52 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ The foundation imports without WinSCP, GnuPG, Active Directory or Exchange depen
| Get-OsUpTime | Local OS uptime and remote Windows CIM queries |
| Remove-SpecialCharacters | Preview or apply a recursive filesystem naming policy |
| Test-RegistryValue | Windows registry value-name existence check |
| Test-IsValidDn | Practical distinguished-name syntax validation |
| Test-IsValidUpn | Practical UPN syntax validation |
| Get-ReportChain | Transitive AD manager report-chain queries |
| New-StringEncryption | Passphrase-based AES-256-GCM string encryption |
| New-StringDecryption | Authenticate and decrypt the versioned string format |
| New-RandomString | Secure random selection from the historical alphabet |
Expand Down Expand Up @@ -64,9 +67,9 @@ Redaction is opt-in and does not guarantee detection of every secret.
- SCP and GnuPG wrappers and bundled WinSCP binaries are removed. Separate modules
will own file transfer and OpenPGP; no replacement is bundled here.
- `Legacy/` retains string encryption, Exchange and script-context helpers for reference.
- `Staging/v3/` retains 3 candidate commands pending tests and compatibility fixes.
These cover report chains, distinguished names and user principal names. They are not currently exported.
- Only the twenty-four listed commands are exported. Private helpers, variables and aliases
- All commands formerly retained in `Staging/v3/` now have supported implementations.
Its README records the migration; separate legacy/staged integrations remain excluded. They are not currently exported.
- Only the twenty-seven listed commands are exported. Private helpers, variables and aliases
are not exported. Existing calls to other v2 commands require the v2 release until
those commands return to the supported API.
- The module GUID and Git history are preserved.
Expand Down Expand Up @@ -324,3 +327,49 @@ registries or select an alternate registry view.

Filesystem tests use real temporary trees. Windows CI additionally exercises real
temporary HKCU keys; those registry integration tests are skipped on Linux/macOS.

## AD naming and report chains

`Test-IsValidDn` checks a documented practical DN syntax, with escaped separators,
hex escapes, descriptor/OID attribute types and multi-valued RDNs. It accepts
non-DC-rooted names and does not restrict attributes to CN/OU/DC. It rejects empty
names/values, literal controls, invalid/dangling escapes, unescaped leading/trailing
value spaces and legacy quoted values. Hex-string notation is checked without BER
validation; decoded escape bytes are not checked for UTF-8 validity. This is not a
complete RFC parser, a canonical comparison or a schema/existence check.

`Test-IsValidUpn` uses a practical ASCII username policy: letters/digits at the
edges, with dots, underscores, hyphens and apostrophes internally, excluding
consecutive dots. The suffix supports DNS/IDN names, long suffixes and single-label
names. This is not email validation or a complete AD/Entra account-creation policy;
it does not check account existence or whether a suffix is configured.

Both validators preserve their parameter aliases, support pipeline input and
return false for explicit null, empty or unsupported input. These policies replace
the old DN regex and email-derived UPN regex; previously accepted/rejected inputs
can change as described above.

```powershell
Test-IsValidDn -DN 'CN=Last\, First,OU=People,DC=example,DC=com'
Test-IsValidUpn -UPN 'first.last@example.technology'
Get-ReportChain -SAM 'manager01' -DomainController 'dc01' -Properties SamAccountName,Mail
```

`Get-ReportChain` retains SAM, UPN and DN identity parameter sets and aliases,
DomainController, and ordered property selection. Its default properties remain
SamAccountName, UserPrincipalName, Mail, Manager and DirectReports; `-Properties '*'`
requests and projects all properties. It resolves exactly one manager, then uses
AD's matching-rule-in-chain filter to retrieve direct and transitive reports,
excluding the manager itself. Result order is the directory's order.

UPN lookup uses an escaped LDAP equality assertion rather than interpolated
PowerShell filter expressions. Manager DN assertion values are escaped separately
from DN string escaping, including UTF-8 bytes. Server and terminating error
behavior are applied to both queries. Missing/ambiguous managers and AD failures
throw rather than returning a warning and undefined results. The caller's error
preference is not changed.

No ActiveDirectory dependency is required to import IT-ToolBox or use the naming
validators. Get-ReportChain requires an available Get-ADUser command and access to
an AD endpoint when invoked; it uses the command's ambient authentication. Tests
mock AD queries and verify filter construction; no live domain query is tested.
Loading
Loading