Skip to content

Instantly share code, notes, and snippets.

@romero126
Last active September 17, 2025 18:19
Show Gist options
  • Select an option

  • Save romero126/ea56740b4e81ba0d82d133b1f17450c2 to your computer and use it in GitHub Desktop.

Select an option

Save romero126/ea56740b4e81ba0d82d133b1f17450c2 to your computer and use it in GitHub Desktop.
ScriptBlockAstLdapFilter
using namespace System.Collections.Generic
using namespace System.Management.Automation.Language
param (
[Parameter(Position = 0, ValueFromPipeline)]
$InputObject = { $_.SamAccountName -eq 'Clark*' }
)
class ScriptBlockLdapFilter : AstVisitor2 {
[System.Text.StringBuilder] $LdapFilter = [System.Text.StringBuilder]::new()
[ScriptBlockAst]$scriptBlockAst
[string] ToString() {
return '('+$this.LdapFilter.ToString()+')'
}
ScriptBlockLdapFilter([ScriptBlockAst]$scriptBlockAst) {
# Constructor that takes a ScriptBlockAst
# It initializes the scriptBlockAst property and starts visiting the AST
$this.scriptBlockAst = $scriptBlockAst
$scriptBlockAst.Visit($this)
}
[AstVisitAction] VisitParenExpression([ParenExpressionAst]$ast) {
# ParenExpressionAst often looks like { ( ... ) }
# We expand this to LDAP filter syntax by adding surrounding parentheses
$null = $this.LdapFilter.Append('(')
$ast.Pipeline.Visit($this)
$null = $this.LdapFilter.Append(')')
return [AstVisitAction]::SkipChildren
}
[AstVisitAction] VisitVariableExpression([VariableExpressionAst]$ast) {
# VariableExpressionAst often looks like { $_ }
# Throws if we try to expand beyond { $_ }
# We can probably do this in MemberExpressionAst but this way grabs more edge cases
if ($ast.VariablePath.UserPath -ne '_') {
throw "Cannot evaluate { $($ast.VariablePath.UserPath) } only `$_ is a supported variable."
return [AstVisitAction]::StopVisit
}
return [AstVisitAction]::Continue
}
[AstVisitAction] VisitMemberExpression([MemberExpressionAst]$ast) {
# MemberExpressionAst often looks like { $_.Property }
# Member.Extent should always be a StringConstantExpressionAst
if ($ast.Member -isnot [StringConstantExpressionAst]) {
throw "Cannot evaluate { $($ast.Member.Extent.Text) } only property access like `$_.Property is supported."
return [AstVisitAction]::StopVisit
}
$null = $this.LdapFilter.Append($ast.Member.Extent.Text)
return [AstVisitAction]::Continue
}
[AstVisitAction] VisitConstantExpression([ConstantExpressionAst]$ast) {
# ConstantExpressionAst often looks like { 15 }
if ($ast.Parent -is [MemberExpressionAst]) {
return [AstVisitAction]::SkipChildren
}
$null = $this.LdapFilter.Append($ast.Value)
return [AstVisitAction]::SkipChildren
}
[AstVisitAction] VisitStringConstantExpression([StringConstantExpressionAst]$ast) {
# StringConstantExpressionAst often looks like { 'value' } or { "value" }
# We only want to process string constants that are not part of a MemberExpressionAst
if ($ast.Parent -is [MemberExpressionAst]) {
return [AstVisitAction]::SkipChildren
}
$null = $this.LdapFilter.Append($ast.Value)
return [AstVisitAction]::Continue
}
[AstVisitAction] VisitBinaryExpression([BinaryExpressionAst]$ast) {
# We need to throw for certain edge cases here.
# Constants cannot be evaluated against a nested expression
if (
$ast.Left -is [BinaryExpressionAst] -and
(
$ast.Right -is [StringConstantExpressionAst] -or
$ast.Right -is [ConstantExpressionAst]
)
) {
throw "Cannot evaluate { $($ast.Right.Extent.Text) } against a nested expression."
return [AstVisitAction]::StopVisit
}
if ($ast.Left -is [StringConstantExpressionAst] -and $ast.Right -is [BinaryExpressionAst]) {
throw "Cannot evaluate { $($ast.Left.Extent.Text) } against a nested expression."
return [AstVisitAction]::StopVisit
}
# Right side should always be a string constant
if (
$ast.Right -isnot [ConstantExpressionAst] -and
$ast.Right -isnot [StringConstantExpressionAst] -and
$ast.Right -isnot [BinaryExpressionAst] -and
$ast.Right -isnot [ParenExpressionAst]
) {
throw "Cannot evaluate { $($ast.Right.Extent.Text) } Right side must be of type constant or nested expression"
return [AstVisitAction]::StopVisit
}
# Apply the And and Or Symbols before processing children
# This ensures nested expressions are properly formatted
if ($ast.Operator -eq 'and') {
$null = $this.LdapFilter.Append('&')
} elseif ($ast.Operator -like 'or') {
$null = $this.LdapFilter.Append('|')
}
# Process Left and Right sides of the binary expression
$null = $this.LdapFilter.Append('(')
$ast.Left.Visit($this)
# Apply the LDAP operator between left and right
switch ($ast.Operator) {
'And' { }
'Or' { }
'ILike' { $null = $this.LdapFilter.Append('=') }
'Like' { $null = $this.LdapFilter.Append('=') }
'Ieq' { $null = $this.LdapFilter.Append('=') }
'Eq' { $null = $this.LdapFilter.Append('=') }
'Ine' { $null = $this.LdapFilter.Append('!=') }
'Ne' { $null = $this.LdapFilter.Append('!=') }
'INotLike' { $null = $this.LdapFilter.Append('!=') }
'NotLike' { $null = $this.LdapFilter.Append('!=') }
'Match' { $null = $this.LdapFilter.Append('~=') }
'IMatch' { $null = $this.LdapFilter.Append('~=') }
'NotMatch' { $null = $this.LdapFilter.Append('!~=') }
'INotMatch'{ $null = $this.LdapFilter.Append('!~=') }
default {
throw "Unsupported operator: $($ast.Operator)"
return [AstVisitAction]::StopVisit
}
}
$ast.Right.Visit($this)
$null = $this.LdapFilter.Append(')')
return [AstVisitAction]::SkipChildren
}
}
$scriptBlockAst = $InputObject.Ast
$visitor = [ScriptBlockLdapFilter]::new($scriptBlockAst)
$visitor.ToString()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment