PowerShell 技能连载 - 使用 AD 过滤器配合 cmdlet(第 3 部分)

在之前的技能中,我们开始学习 ActiveDirectory 模块(免费的 RSAT 工具)中的 cmdlet 如何过滤执行结果,并且开始以我们的方式插入快捷且完善的 LDAP 过滤器。

LDAP 过滤器有一个强制的需求。您必须使用原始的 ActiveDirectory 属性名,而不是许多 PowerShell cmdlet 中的友好名称。所以 “country” 需要改为 AD 的属性名 “co”。当您坚持使用这些名字后,创建 LDAP 过滤器十分容易。

这行代码将会从 Active Directory 中获取所有 Windows 10 计算机:

1
2
Get-ADComputer -LDAPFilter '(operatingSystem=*10*)' -Properties operatingSystem |
Select-Object samaccountname, operatingSystem

如果您想合并多个过滤器,请将它们加到小括号中,然后在前面添加 “&” 进行逻辑与操作,添加 “|” 进行逻辑或操作。所以这行代码查找所有从 Wuppertal 城市,名字以 “A” 开头的用户:

1
Get-ADUser -LDAPFilter '(&(l=Wuppertal)(name=a*))'

PowerShell 技能连载 - 使用 AD 过滤器配合 cmdlet(第 2 部分)

在前一个技能中,我们开始学习 ActiveDirectory 模块(免费的 RSAT 工具)中的 cmdlet 如何过滤执行结果。您学到了过滤器看起来像 PowerShell 代码,但是实际上不是。

对于简单的过滤,该过滤器工作正常。然而,当您使用操作符以外的 PowerShell 语言特性时,您很快会发现实际中使用的过滤器并不是 PowerShell 代码。

如果您想获得一个未配置文件路径的 AD 用户列表,您可能会好奇地尝试以下的代码:

1
2
Get-ADUser -Filter { profilePath -eq $null} -ResultSetSize 5
Get-ADUser -Filter { profilePath -eq ''} -ResultSetSize 5

两个过滤器都会失败。PowerShell 报告 $null 变量是未知的。而在第二行中,报告该该搜索查询不合法。

这是为什么大多数情况下使用在 Active Directory 常见的原生 LDAPFilters 是更方便(而且更快)的。LDAP 过滤器是括号中的表达式。它们包含一个名称和一个操作符。以下代码返回前 5 个有配置文件路径的用户:

1
Get-ADUser -LDAPFilter '(profilePath=*)' -ResultSetSize 5

加上 “!” 以后,可以将结果反转,所以以下代码将返回前 5 个没有配置文件路径的用户:

1
Get-ADUser -LDAPFilter '(!profilePath=*)' -ResultSetSize 5

以下代码将返回用户和他们的配置文件路径的列表:

1
2
Get-ADUser -LDAPFilter '(profilePath=*)' -Properties profilePath |
Select-Object samaccountName, profilePath

PowerShell 技能连载 - 使用 AD 过滤器配合 cmdlet(第 1 部分)

ActiveDirectory Powershell 模块中包含了免费的 RSAT 工具。您可以使用这个模块中的 cmdlet 来获取 AD 信息,例如用户或者组名。例如 Get-ADUserGet-ADComputer 等 cmdlet 支持服务端过滤器。不过,它们的工作方式可能和您设想的略有不同。

对于简单的查询,这些过滤器使用起来很容易。例如,这行代码筛选出前五个名字以 “A” 开头的用户,而且过滤器的语法看起来很像 PowerShell 代码:

1
Get-ADUser -Filter { name -like 'A*' } -ResultSetSize 5

不过它还不是真的 PowerShell 语法:-Filter 参数接受的是纯文本,所以您也可以使用引号代替大括号:

1
Get-ADUser -Filter " name -like 'A*' " -ResultSetSize 5

使用大括号(脚本块)仍是一个好主意,因为大括号包括的是 PowerShell 代码,所以在大括号内写代码时可以获得代码高亮和语法错误特性。脚本块稍后可以方便地转换为字符串(Get-ADUser 会自动处理)。

PowerShell 技能连载 - 创建临时文件名

当您需要写入信息到磁盘时,使用唯一的临时文件名是合理的。如果您使用静态的文件名并且多次运行您的代码,会一次又一次地覆盖写入同一个文件。如果某个人打开该文件并锁定它,将导致脚本执行失败。

以下是一些简单的生成唯一的临时文件名的方法:

1
2
3
4
5
6
7
8
9
10
11
# use a random number (slight chance of duplicates)
$path = "$env:temp\liste$(Get-Random).csv"
"Path is: $path"

# use a GUID. Guaranteed to be unique but somewhat hard on the human eye
$path = "$env:temp\liste$([Guid]::NewGuid().toString()).csv"
"Path is: $path"

# use timestamp with milliseconds
$path = "$env:temp\liste$(Get-Date -format yyyy-MM-dd_HHmmss_ffff).csv"
"Path is: $path"

PowerShell 技能连载 - 文件系统压力测试

如果您想生成一个超大文件来做压力测试,您不需要浪费时间写入大量数据到一个文件中,使它体积增大。相反,只需要设置一个期望的文件大小来占据磁盘空间即可。

以下是创建一个 1GB 测试文件的方法:

1
2
3
4
5
6
7
8
9
10
# create a test file
$path = "$env:temp\dummyFile.txt"
$file = [System.IO.File]::Create($path)

# set the file size (file uses random content)
$file.SetLength(1gb)
$file.Close()

# view file properties
Get-Item $path

PowerShell 技能连载 - 通过参数传递命令

函数参数有一个很少见的用法:用户可以传入一个输出命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Get-ProcessList
{
param
(
[string]
[ValidateSet('GridView','String')]
$OutputMode = 'String'
)

Get-Process | & "Out-$OutputMode"

}

# output as a string
Get-ProcessList -OutputMode String
# output in a grid view window
Get-ProcessList -OutputMode GridView

PowerShell 技能连载 - 处理文件编码和 BOM

当您将文本内容写入到文件时,PowerShell cmdlet 可以指定文件编码。编码决定了字符的存储方式,并且当某个字符显示乱码时,通常是由于编码不匹配造成的。

然而,有一些编码设置是无法通过 cmdlet 参数控制的。以下是一个例子。将一个进程列表保存到 CSV 文件:

1
2
3
4
$Path = "$env:temp\export.csv"

Get-Process |
Export-CSV -NoTypeInformation -UseCulture -Encoding UTF8 -Path $Path

您现在可以在 Excel 或任意文本编辑器中打开生成的 CSV 文件。当您使用 Notepad++ 打开文件时,状态栏显示编码格式为:UTF-8-BOM。

这段 PowerShell 代码以 UTF-8 编码生成文件,所以这段没有问题。BOM 代表“字节顺序标记” (Byte Order Mark)。当使用 BOM 时,将在文件的起始处增加一个特定的字节顺序标识,这样程序可以自动识别使用的编码格式。

不过,一些编辑器和数据处理系统无法处理 BOM。要移除 BOM 并使用纯文本编码,请使用类似这样的 PowerShell 代码:

1
2
3
4
5
6
function Remove-BomFromFile($OldPath, $NewPath)
{
$Content = Get-Content $OldPath -Raw
$Utf8NoBomEncoding = New-Object System.Text.UTF8Encoding $False
[System.IO.File]::WriteAllLines($NewPath, $Content, $Utf8NoBomEncoding)
}

在上述例子中,要将 UTF-8-BOM 转化为纯 UTF-8,请运行这段代码:

1
2
3
$Path = "$env:temp\export.csv"
$NewPath = "$env:temp\export_new.csv"
Remove-BomFromFile -OldPath $Path -NewPath $NewPath

PowerShell 技能连载 - 查找嵌套的 Active Directory 成员(第 3 部分)

在前一个技能中我们演示了如何使用 ActiveDirectory 模块中的 cmdlet 来查找某个 Active Directory 用户所有直接和间接的成员。如果您想知道当前用户的成员信息,还有一个更简单(而且更快)的方法:用当前用户的 access token 来获取当前有效的组成员:

1
2
3
4
5
$groups = [System.Security.Principal.WindowsIdentity]::GetCurrent().Groups.Translate([System.Security.Principal.NTAccount])

$groups

$groups.Count

PowerShell 技能连载 - 查找嵌套的 Active Directory 成员(第 2 部分)

在前一个技能中我们演示了如何使用 ActiveDirectory 模块中的 cmdlet 来查找某个 Active Directory 用户所有直接和间接的成员。

如果您没有权限操作 ActiveDirectory 模块,PowerShell 也可以使用纯 .NET 方法来获取成员:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function Get-NestedGroupMember
{
param
(
[Parameter(Mandatory,ValueFromPipeline)]
[string]
$distinguishedName
)

process
{

$DomainController = $env:logonserver.Replace("\\","")
$Domain = New-Object System.DirectoryServices.DirectoryEntry("LDAP://$DomainController")
$Searcher = New-Object System.DirectoryServices.DirectorySearcher($Domain)
$Searcher.PageSize = 1000
$Searcher.SearchScope = "subtree"
$Searcher.Filter = "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:=$distinguishedName))"
# attention: property names are case-sensitive!
$colProplist = "name","distinguishedName"
foreach ($i in $colPropList){$Searcher.PropertiesToLoad.Add($i) | Out-Null}
$all = $Searcher.FindAll()

$all.Properties | ForEach-Object {
[PSCustomObject]@{
# attention: property names on right side are case-sensitive!
Name = $_.name[0]
DN = $_.distinguishedname[0]
} }
}
}

# make sure you specify a valid distinguishedname for a user below
Get-NestedGroupMember -distinguishedName 'CN=UserName,DC=powershell,DC=local'

PowerShell 技能连载 - 查找嵌套的 Active Directory 成员(第 1 部分)

ActiveDirectory 模块(免费的 RSAT 工具的一部分)提供许多 AD cmdlet。其中一个可以读取整个直接组的成员,例如:

1
PS> Get-ADPrincipalGroupMembership  -Identity $env:username

然而,这个 cmdlet 无法列出间接组的成员,而且它还有一个 bug:在某些场景下,它只是报告“未知错误”。

这是一个简单的读取所有组成员(包括间接成员)的替代方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function Get-NestedGroupMember
{
param
(
[Parameter(Mandatory,ValueFromPipeline)]
[string]
$Identity
)

process
{
$user = Get-ADUser -Identity $Identity
$userdn = $user.DistinguishedName
$strFilter = "(member:1.2.840.113556.1.4.1941:=$userdn)"
Get-ADGroup -LDAPFilter $strFilter -ResultPageSize 1000
}
}


Get-NestedGroupMember -Identity $env:username |
Select-Object -Property Name, DistinguishedName