PowerShell 技能连载 - 更好的递归

当函数调用自身时,称为“递归”。当脚本想要遍历文件系统的一部分时,您会经常看到这种技术:一个函数处理文件夹内容,当它遇到子文件夹时,它会调用自身。

递归的功能很强大,但是却很难调试,并且有潜在的危险。因为当您犯错时,您将陷入无休止的循环。此外,递归深度过高时,始终存在堆栈溢出的风险。

许多通常需要递归的任务也可以通过使用“队列”来设计:当您的代码遇到新任务时,无需再次调用自身,而是将新任务放在队列中,一旦完成初始任务,开始解决队列中的任务。

感谢 Lee Holmes,这是一个遍历整个驱动器 C:\ 但不使用递归的简单示例。相反,您可以看到正在使用的队列:

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
# get a new queue
[System.Collections.Queue]$queue = [System.Collections.Queue]::new()
# place the initial search path(s) into the queue
$queue.Enqueue('c:\')
# add as many more search paths as you need
# they will eventually all be traversed
#$queue.Enqueue('D:\')

# while there are still elements in the queue...
while ($queue.Count -gt 0)
{
# get one item off the queue
$currentDirectory = $queue.Dequeue()
try
{
# find all subfolders and add them to the queue
# a classic recurse approach would have called itself right here
# this approach instead pushes the future tasks just onto
# the queue for later use
[IO.Directory]::GetDirectories($currentDirectory) | ForEach-Object {
$queue.Enqueue($_)
}
}
catch {}

try
{
# find all files in this folder with the given extensions
[IO.Directory]::GetFiles($currentDirectory, '*.psm1')
[IO.Directory]::GetFiles($currentDirectory, '*.ps1')
}
catch{}
}
评论

PowerShell 技能连载 - 管理本地组成员(第 2 部分)

在上一个技巧中,我们解释了为什么访问本地组成员并不总是与内置的 cmdlet(如 Get-LocalGroupMember)一起使用,以及使用旧的(但仍可正常使用)ADSI 接口解决该问题的方法。

如果您想以此为基础构建解决方案,您可能想知道如何将本地帐户添加到组或从组中删除,以及如何启用和禁用本地管理员帐户。

这是说明这些方法的几行有用的代码。您可以独立使用这些代码,也可以将它们集成到自己的脚本逻辑中。

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
35
36
37
38
39
40
41
42
43
44
45
46
# these examples use the data below - adjust to your needs
# DO NOT RUN THESE LINES UNLESS YOU CAREFULLY
# REVIEWED AND YOU KNOW WHAT YOU ARE DOING!

# use local machine
$ComputerName = $env:computername
# find name of local Administrators group
$Group = ([Security.Principal.SecurityIdentifier]'S-1-5-32-544').Translate([System.Security.Principal.NTAccount]).Value.Split('\')[-1]
# find name of local Administrator user
$Admin = Get-CimInstance -ClassName Win32_UserAccount -Filter "LocalAccount = TRUE and SID like 'S-1-5-%-500'"
$UserName = $Admin.Name
# examples

# find all local groups
$computerObj = [ADSI]("WinNT://$ComputerName,computer")
$computerObj.psbase.children |
Where-Object { $_.psbase.schemaClassName -eq 'group' } |
Select-Object -Property @{N='Name';E={$_.Name[0]}},
Path,
@{N='Sid';E={[Security.Principal.SecurityIdentifier]::new($_.objectSid.value,0).Value}}



# find members of local admin group
$computerObj = [ADSI]("WinNT://$ComputerName,computer")
$groupObj = $computerObj.psbase.children.find($Group, 'Group')
$groupObj.psbase.Invoke('Members') |
ForEach-Object { $_.GetType().InvokeMember('ADspath','GetProperty',$null,$_,$null) }

# add user to group/remove from group
$computerObj = [ADSI]("WinNT://$ComputerName,computer")
$groupObj = $computerObj.psbase.children.find($Group, 'Group')
# specify the user or group to add or remove
$groupObj.Add('WinNT://DOMAIN/USER,user')
$groupObj.Remove('WinNT://DOMAIN/USER,user')

# enabling/disabling accounts
$computerObj = [ADSI]("WinNT://$ComputerName,computer")
$userObj = $computerObj.psbase.children.find($UserName, 'User')
#enable
$userObj.UserFlags=$userObj.UserFlags.Value -band -bnot 512
$userObj.CommitChanges()

#disable
$userObj.UserFlags=$userObj.UserFlags.Value -bor 512
$userObj.CommitChanges()
评论

PowerShell 技能连载 - 管理本地组成员(第 1 部分)

幸运的是,PowerShell 5 及更高版本附带了诸如 Get-LocalGroupMember 之类的 cmdlet,该 cmdlet 列出了本地组的成员。不幸的是,这些 cmdlet 有一个缺陷:如果组包含一个或多个孤立成员,则该 cmdlet 将无法列出任何组成员。

孤立的组成员可以是已添加到组中但后来在 Active Directory 中删除的用户或组。这样的孤儿显示为 SID 号,而不是对话框中的名称。

若要解决此问题并列出组成员而不考虑孤立帐户,请尝试使用 Get-GroupMemberLocal 函数,该函数使用 PowerShell 自带 cmdlet 之前前通常使用的,旧的 ADSI 方法:

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
35
36
37
38
39
40
function Get-GroupMemberLocal
{
[CmdletBinding(DefaultParameterSetName='Name')]
param
(
[Parameter(Mandatory,Position=0,ParameterSetName='Name')]
[string]
$Name,

[Parameter(Mandatory,Position=0,ParameterSetName='Sid')]
[System.Security.Principal.SecurityIdentifier]
$Sid,

[string]
$Computer = $env:COMPUTERNAME
)

if ($PSCmdlet.ParameterSetName -eq 'Sid')
{
$Name = $sid.Translate([System.Security.Principal.NTAccount]).Value.Split('\')[-1]
}

$ADSIComputer = [ADSI]("WinNT://$Computer,computer")
$group = $ADSIComputer.psbase.children.find($Name, 'Group')
$group.psbase.invoke("members") |
ForEach-Object {
try
{
$disabled = '-'
$disabled = $_.GetType().InvokeMember("AccountDisabled", 'GetProperty', $null, $_, $null)
} catch {}
[PSCustomObject]@{
Name = $_.GetType().InvokeMember("Name", 'GetProperty', $null, $_, $null)
SID = [Security.Principal.SecurityIdentifier]::new($_.GetType().InvokeMember("objectSid", 'GetProperty', $null, $_, $null),0)
Path = $_.GetType().InvokeMember("AdsPath", 'GetProperty', $null, $_, $null)
Type = $_.GetType().InvokeMember("Class", 'GetProperty', $null, $_, $null)
Disabled = $disabled
}
}
}

下面是一个实际示例:让我们转储本地管理员组。您可以按名称或不区分文化的 SID 来访问组:

1
2
3
4
5
6
7
8
PS> Get-GroupMemberLocal -Sid S-1-5-32-544 | Format-Table

Name SID Path Type Disabled
---- --- ---- ---- --------
Administrator S-1-5-21-2770831484-2260150476-2133527644-500 WinNT://WORKGROUP/DELL7390/Administrator User True
Presentation S-1-5-21-2770831484-2260150476-2133527644-1007 WinNT://WORKGROUP/DELL7390/Presentation User False
RemoteAdmin S-1-5-21-2770831484-2260150476-2133527644-1013 WinNT://WORKGROUP/DELL7390/RemoteAdmin User False
Management S-1-5-21-2770831484-2260150476-2133527644-1098 WinNT://WORKGROUP/DELL7390/Management Group -
评论

PowerShell 技能连载 - 标识本地管理员帐户的名称

有时,PowerShell 脚本需要访问或使用内置的 Administrator 帐户或内置的 Administrators组。不幸的是,它们的名称已本地化,因此它们的名称可以根据 Windows 操作系统的语言进行更改。

但是,它们确实使用了恒定的(众所周知的)SID(安全标识符)。通过使用 SID,您可以获得名称。对于本地 Administrator组,这很简单,因为 SID 始终是已知的:S-1-5-32-544。使用一行代码,可以翻译 SID。这是从德文系统获得的结果:

1
2
PS> ([Security.Principal.SecurityIdentifier]'S-1-5-32-544').Translate([System.Security.Principal.NTAccount]).Value
VORDEFINIERT\Administratoren

使用内置管理员等帐户,就不那么简单了。在此,只有 RID(相对标识符)是已知的:-500。

通过简单的WMI查询,您将获得与过滤器匹配的帐户:

1
2
3
4
5
PS> Get-CimInstance -ClassName Win32_UserAccount -Filter "LocalAccount = TRUE and SID like 'S-1-5-%-500'"

Name Caption AccountType SID Domain
---- ------- ----------- --- ------
Administrator DELL7390\Administrator 512 S-1-5-21-2770831484-2260150476-2133527644-500 DELL7390
评论

PowerShell 技能连载 - 读取已安装的软件(第 2 部分)

在上一个技能中,我们演示了 Get-ItemProperty 强大的功能,并且您可以通过读取多个注册表位置而仅用一行代码来创建整个软件清单。

今天,让我们在软件清单中添加两个重要的信息:范围(是为每个用户安装的软件还是为所有用户安装的软件?)和体系结构(x86或x64)。

每个信息都无法在注册表键值中找到,而是通过确定信息在注册表中的存储位置来找到。这是 Get-ItemProperty 提供的另一个巨大好处:它不仅返回给定注册表项的注册表值。它还添加了许多属性,例如 PSDrive(注册表配置单元)和 PSParentPath(正在读取注册表项的路径)。

以下是根据发现信息的地方将范围和体系结构信息添加到软件清单的解决方案:

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
35
36
37
38
39
40
41
42
43
44
45
46
47
# Registry locations for installed software
$paths =
# all users x64
'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*',
# all users x86
'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*',
# current user x64
'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*',
# current user x86
'HKCU:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'

# calculated properties

# AllUsers oder CurrentUser?
$user = @{
Name = 'Scope'
Expression = {
if ($_.PSDrive -like 'HKLM')
{
'AllUsers'
}
else
{
'CurrentUser'
}
}
}

# 32- or 64-Bit?
$architecture = @{
Name = 'Architecture'
Expression = {
if ($_.PSParentPath -like '*\WOW6432Node\*')
{
'x86'
}
else
{
'x64'
}
}
}
Get-ItemProperty -ErrorAction Ignore -Path $paths |
# eliminate reg keys with empty DisplayName
Where-Object DisplayName |
# select desired properties and add calculated properties
Select-Object -Property DisplayName, DisplayVersion, $user, $architecture
评论

PowerShell 技能连载 - 读取已安装的软件(第 1 部分)

Get-ItemProperty cmdlet 可以比大多数用户知道的功能强大得多的方式读取注册表值。该 cmdlet 支持多个注册表路径,并且支持通配符。这样,只需一行代码即可从四个注册表项读取所有已安装的软件(及其卸载字符串):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# list of registry locations where installed software is stored
$paths =
# all users x64
'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*',
# all users x86
'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*',
# current user x64
'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*',
# current user x86
'HKCU:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'

Get-ItemProperty -ErrorAction Ignore -Path $paths |
# eliminate all entries with empty DisplayName
Where-Object DisplayName |
# select some properties (registry values)
Select-Object -Property DisplayName, DisplayVersion, UninstallString, QuietUninstallString
评论

PowerShell 技能连载 - 保持 Windows 和 PowerShell 持续运行

根据 Windows PC 的电源设置,即使您在运行冗长的脚本,您的计算机仍可能在一段时间后进入待机或休眠状态。

确保 Windows 在脚本忙时继续运行的一种方法是使用“演示模式”。您可以使用一个工具来启用和禁用它。在 PowerShell 中,运行以下命令:

1
PS> presentationsettings

这将打开一个窗口,您可以在其中检查和控制当前的演示文稿设置。要自动启动和停止演示模式,该命令支持参数 /start/stop

为了确保 Windows 在脚本运行时不会进入休眠状态,请将其放在第一行脚本中:

1
PS> presentationsettings /start

在脚本末尾,关闭演示模式,如下所示:

1
PS> presentationsettings /stop

要验证这两个命令的效果,请运行不带参数的命令,并选中对话框中最上方的复选框。

评论

PowerShell 技能连载 - 读取事件日志(第 4 部分)

在上一个技巧中,我们鼓励您弃用 Get-EventLog cmdlet,而开始使用 Get-WinEvent,因为后者功能更强大,并且在PowerShell 7 中不再支持前者。

如上例所示,通过 Get-WinEvent 查询事件需要一个哈希表。例如,以下命令返回已安装更新的列表:

1
2
3
4
5
Get-WinEvent -FilterHashtable @{
LogName = 'System'
ProviderName = 'Microsoft-Windows-WindowsUpdateClient'
Id = 19
}

实际上,事件数据始终使用 XML 格式存储,并且所有查询都使用 XPath 过滤器查询来为您检索数据。如果您是 XML 和XPath专家,则可以直接使用以下命令来获得相同的结果:

1
2
3
Get-WinEvent -FilterXML @'
<QueryList><Query Id="0" Path="system"><Select Path="system">*[System/Provider[@Name='microsoft-windows-windowsupdateclient'] and (System/EventID=19)]</Select></Query></QueryList>
'@

哈希表是一个很方便的快捷方式。在内部,哈希表中包含的信息将转换为上面的 XML 语句。幸运的是,将哈希表转换为 XML 一点也不困难,因为 Get-WinEvent 会为您做到这一点:只需提交一个哈希表,并要求返回 XML 语句:

1
2
3
4
5
6
7
$result = Get-WinEvent -FilterHashtable @{
LogName = 'System'
ProviderName = 'Microsoft-Windows-WindowsUpdateClient'
Id = 19
} -MaxEvents 1 -Verbose 4>&1

$result | Where-Object { $_ -is [System.Management.Automation.VerboseRecord] }

本质上,通过提交 -Verbose 参数,要求 Get-WinEvent 将计算出的XML语句返回给您。通过将管道 4 重定向到输出管道 1,您可以将详细消息捕获到 $result 并过滤详细消息。这样,您可以捕获计算出的XML:

VERBOSE: Found matching provider: Microsoft-Windows-WindowsUpdateClient
VERBOSE: The Microsoft-Windows-WindowsUpdateClient provider writes events to the System log.
VERBOSE: The Microsoft-Windows-WindowsUpdateClient provider writes events to the Microsoft-Windows-WindowsUpdateClient/Operational log.
VERBOSE: Constructed structured query:
VERBOSE: <QueryList><Query Id="0" Path="system"><Select Path="system">*[System/Provider[@Name='microsoft-windows-windowsupdateclient'] and
VERBOSE: (System/EventID=19)]</Select></Query></QueryList>.
评论

PowerShell 技能连载 - 读取事件日志(第 3 部分)

在上一个技能中,我们鼓励您弃用 Get-EventLog cmdlet,而开始使用 Get-WinEvent——因为后者功能更强大,并且在 PowerShell 7 中不再支持前者。

Win-EventLog 相比,Get-WinEvent 的优点之一是它能够读取所有 Windows 事件日志,而不仅仅是经典事件日志。要找出这些其他事件日志的名称,请尝试以下操作:

1
2
3
4
Get-WinEvent -ListLog * -ErrorAction Ignore |
# ...that have records...
Where-Object RecordCount -gt 0 |
Sort-Object -Property RecordCount -Descending

这将返回系统上所有包含数据的事件日志的列表,并按记录的事件数进行排序。显然,诸如“系统”和“应用程序”之类的“经典”日志比较显眼,但是还有许多其他有价值的日志,例如“具有高级安全性/防火墙的 Microsoft-Windows-Windows 防火墙”。让我们检查其内容:

1
2
3
Get-WinEvent -FilterHashtable @{
LogName = 'Microsoft-Windows-Windows Firewall With Advanced Security/Firewall'
} -MaxEvents 20

由于我的系统正在使用内置防火墙,因此结果将返回有关更改防火墙规则和其他配置历史记录的详细信息。

使用不推荐使用的 Get-EventLog 将无法获得此信息。

评论

PowerShell 技能连载 - 读取事件日志(第 2 部分)

在上一个技能中,我们鼓励您弃用 Get-EventLog cmdlet,而开始使用 Get-WinEvent——因为后者功能更强大,并且在 PowerShell 7 中不再支持前者。

让我们再次练习如何将 Get-EventLog 语句转换为 Get-WinEvent。这是我想翻译的一行代码。它从过去 48 小时内发生的系统事件日志中返回所有错误和警告:

1
$twoDaysAgo = (Get-Date).AddDays(-2)Get-EventLog -LogName System -EntryType Error, Warning -After $twoDaysAgo

这将是在所有 PowerShell 版本中均可使用的 Get-WinEvent 单行代码:

1
2
$twoDaysAgo = (Get-Date).AddDays(-2)Get-WinEvent -FilterHashtable @{
LogName = 'System' Level = 2,3 StartTime = $twoDaysAgo }

它返回相同的事件,但是速度更快。以下是您可以在哈希表中使用的其余键:

col 1 col 2 col 3
Key name Data Type Wildcards Allowed
LogName <String[]> Yes
ProviderName <String[]> Yes
Path <String[]> No
Keywords <Long[]> No
ID <Int32[]> No
Level <Int32[]> No
StartTime No
EndTime No
UserID No
Data <String[]> No
评论