PowerShell 技能连载 - 排序技巧(第 4 部分)

在上一个技能中,我们展示了 Sort-Object 通过属性名称、哈希表或普通脚本块来排序,并且我们使用脚本块来控制排序算法,并按日期和时间而不是字母数字对字符串信息进行排序。

在这最后一个示例中,让我们使用它来对 IPv4 地址进行排序。默认情况下,Sort-Object 将它们视为纯文本并使用字母排序:

1
2
3
4
PS> '10.12.11.1', '298.12.11.112', '8.8.8.8' | Sort-Object
10.12.11.1
298.12.11.112
8.8.8.8

要正确排序 IPv4 地址,您可以将它们转换为 [version] 类型,该类型也包含四个数字:

1
2
3
4
5
PS> '10.12.11.1', '298.12.11.112', '8.8.8.8' | Sort-Object -Property { $_ -as [version] }

8.8.8.8
10.12.11.1
298.12.11.112

PowerShell 技能连载 - 排序技巧(第 3 部分)

在上一个技能中,我们展示了 Sort-Object 接受属性名称、哈希表或普通脚本块来对事物进行排序。让我们看看为什么将脚本块提交给 Sort-Object 是个好主意。

假设您有一堆表示日期的字符串数据,并且您想对它们进行排序:

1
2
3
4
5
PS> 'May 12, 2020', 'Feb 1, 1999', 'June 12, 2021' | Sort-Object

Feb 1, 1999
June 12, 2021
May 12, 2020

结果已排序,但不按日期排序。由于输入是字符串,Sort-Object 使用字母数字排序算法。您现在可以将原始数据转换为另一种格式并对其进行排序。

但是,您也可以要求 Sort-Object 进行转换。不同之处在于原始数据格式保持不变:一系列字符串将按日期排序,但仍然是字符串:

1
2
3
4
5
6
PS> 'May 12, 2020', 'Feb 1, 1999', 'June 12, 2021' |
Sort-Object -Property { [DateTime]$_ }

Feb 1, 1999
May 12, 2020
June 12, 2021

当然,您的工作是想出一个脚本块,将 $_ 中传入的原始数据正确转换为您想要用于排序的所需类型。对于日期,您可能还想查看使用本地日期和时间格式的 -as 运算符,而直接转换始终使用美国格式:

1
2
3
4
5
PS> 'May 12, 2020', 'Feb 1, 1999', 'June 12, 2021' | Sort-Object -Property { $_ -as [DateTime] }

Feb 1, 1999
May 12, 2020
June 12, 2021

PowerShell 技能连载 - 排序技巧(第 2 部分)

在上一个技能中,我们展示了 Sort-Object 如何对多个属性进行排序,以及如何使用哈希表分别控制每个属性的排序方向。不过,哈希表还可以做更多的事情。

例如,哈希表键 “Expression“ 可以是一个脚本块,然后针对您要排序的每个项目执行该脚本块。脚本块的结果决定了排序顺序。

这就是为什么这行代码每次都会以不同的方式重新排列数字列表:

1
1..10 | Sort-Object -Property @{Expression={ Get-Random }}

本质上,本示例使用 Get-Random 的随机结果对数字列表进行随机排序。这可能很有用,即当您使用密码生成器并希望随机分配脚本计算的字符时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# compose password out of these
$Capitals = 2
$Numbers = 1
$lowerCase = 3
$Special = 1

# collect random chars from different lists in $chars
$chars = & {
'ABCDEFGHKLMNPRSTUVWXYZ'.ToCharArray() | Get-Random -Count $Capitals
'23456789'.ToCharArray() | Get-Random -Count $Numbers
'abcdefghkmnprstuvwxyz'.ToCharArray() | Get-Random -Count $lowerCase
'!§$%&?=#*+-'.ToCharArray() | Get-Random -Count $Special
} | # <- don't forget pipeline symbol!
# sort them randomly
Sort-Object -Property { Get-Random }

# convert chars to one string
$password = -join $chars
$password

正如您在实际示例中看到的,Sort-Object 还接受一个简单的脚本块,它表示哈希表的 “Expression“ 键。两行的工作方式相同(但当然会产生不同的随机结果):

1
1..10 | Sort-Object -Property @{Expression={ Get-Random }}1..10 | Sort-Object -Property { Get-Random }

PowerShell 技能连载 - 排序技巧(第 1 部分)

Sort-Object 是用于排序的 cmdlet:只需指定要排序的属性,Sort-Object 涵盖其余部分,包括根据属性数据类型选择正确的排序算法:

1
Get-Service | Sort-Object -Property DisplayName -Descending

一个鲜为人知的事实是 Sort-Object 也接受哈希表,这给了你更多的控制权。例如,您可以像这样轻松地对多个属性进行排序:

1
Get-Service | Sort-Object -Property Status, DisplayName -Descending

但是,如果您想控制每个属性的排序方向,则需要一个哈希表。此示例按降序对状态进行排序,但按升序对显示名称进行排序:

1
2
3
4
5
6
$displayName = @{
Expression = "DisplayName"
Descending = $false
}

Get-Service | Sort-Object -Property Status, $displayName -Descending

PowerShell 技能连载 - 对文本做哈希

PowerShell 附带了 Get-FileHash 命令,它读取文件并计算唯一的哈希值。这非常适合测试文件是否具有相同的内容。但是,无法对纯文本进行哈希处理。

当然,您可以将要哈希的文本写入文件,然后使用 Get-FileHash

更好的方法是 Get-FileHash 的一个很少人知道的功能。除了传入文件路径之外,您还可以提交所谓的 “MemoryStream“,当您将文本加载内存流,再调用命令,就能得到哈希值:

1
2
3
4
5
6
7
8
9
10
11
$text = 'this is a test'

$memoryStream = [System.IO.MemoryStream]::new()
$streamWriter = [System.IO.StreamWriter]::new($MemoryStream)
$streamWriter.Write($text)
$streamWriter.Flush()
$memoryStream.Position = 0
$hash = Get-FileHash -InputStream $MemoryStream -Algorithm 'SHA1'
$memoryStream.Dispose()
$streamWriter.Dispose()
$hash.Hash

只是不要忘记在使用后处理 MemoryStreamStreamWriter 对象以释放内存。

PowerShell 技能连载 - 检测 Windows 版本(第 3 部分)

在上一个技巧中,我们说明访问 WinRT 类 AnalyticsInfo 似乎是读取当前 Windows 10 版本的唯一受支持方式。与使用上一个示例中的异步方法不同,为了仅获取当前的 Windows 10 版本,这里有一个更简单的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# get raw Windows version
[int64]$rawVersion =
[Windows.System.Profile.AnalyticsInfo,Windows.System.Profile,ContentType=WindowsRuntime].
GetMember('get_VersionInfo').Invoke( $Null, $Null ).DeviceFamilyVersion

# decode bits to version bytes
$major = ( $rawVersion -band 0xFFFF000000000000l ) -shr 48
$minor = ( $rawVersion -band 0x0000FFFF00000000l ) -shr 32
$build = ( $rawVersion -band 0x00000000FFFF0000l ) -shr 16
$revision = $rawVersion -band 0x000000000000FFFFl

# compose version
$winver = [System.Version]::new($major, $minor, $build, $revision)
$winver

请注意,PowerShell 7 (pwsh.exe) 无法访问此 API。该代码需要 Windows PowerShell (powershell.exe)。

PowerShell 技能连载 - 检测 Windows 版本(第 2 部分)

在之前的技能中,我们报告了 ReleaseId 已弃用,无法再用于正确识别当前的 Windows 10 版本。相反,应该使用 DisplayVersion

1
2
PS> (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion').DisplayVersion
20H2

但是,DisplayVersion 也不是确定当前 Windows 10 版本的可靠方法,因为其最初的目的是使外壳能够向用户报告当前版本。它也可能在未来发生变化。

识别当前 Windows 10 版本的唯一受支持的安全方法是使用名为 AnalyticsInfo 的操作系统类。不过用起来比较复杂,因为该类在 WinRT 中异步运行。PowerShell 7 (pwsh.exe) 无法访问此类。但是,内置的 Windows PowerShell (powershell.exe) 可以创建包装器并返回信息:

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
# load WinRT and runtime types
[System.Void][Windows.System.Profile.AnalyticsInfo,Windows.System.Profile,ContentType=WindowsRuntime]
Add-Type -AssemblyName 'System.Runtime.WindowsRuntime'

# define call and information to query
[Collections.Generic.List[System.String]]$names = 'DeviceFamily',
'OSVersionFull',
'FlightRing',
'App',
'AppVer'

$task = [Windows.System.Profile.AnalyticsInfo]::GetSystemPropertiesAsync($names)

# use reflection to find method definition
$definition = [System.WindowsRuntimeSystemExtensions].GetMethods().Where{
$_.Name -eq 'AsTask' -and
$_.GetParameters().Count -eq 1 -and
$_.GetParameters()[0].ParameterType.Name -eq 'IAsyncOperation`1'
}

# create generic method
$Method = $definition.MakeGenericMethod( [System.Collections.Generic.IReadOnlyDictionary[System.String,System.String]] )

# call async method and wait for completion
$task = $Method.Invoke.Invoke($null, $task)
$null = $task.Wait(-1)

# emit output
$task.Result

结果类似于:

Key           Value
---           -----
OSVersionFull 10.0.19042.985.amd64fre.vb_release.191206-1406
FlightRing    Retail
App           powershell_ise.exe
AppVer        10.0.19041.1
DeviceFamily  Windows.Desktop

OSVersionFull 返回有关当前 Windows 版本的完整详细信息。

请注意,上面示例中调用的方法可以检索更多详细信息。$names 列出要查询的属性名称。不幸的是,没有办法发现可用的属性名称,因为第三方方可能会添加无穷无尽的附加信息。本示例中使用的五个属性是唯一保证的属性。

PowerShell 技能连载 - 检测 Windows 版本(第 1 部分)

可以从 Windows 注册表轻松读取当前的 Windows 版本:

1
2
3
4
5
PS> Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' | Select-Object -Property ReleaseId, DisplayVersion

ReleaseId DisplayVersion
--------- --------------
2009 20H2

请注意,不推荐使用 ReleaseId 中显示的信息。新的 Windows 10 版本改为使用 DisplayVersion 属性。要正确识别 Windows 10 版本,请确保使用 DisplayVersion 而不是 ReleaseId

在上面的示例中,您可以看到 Windows 10 报告了许多不同版本的相同 ReleaseId (2009)。例如,DisplayVersion 20H1 也使用 ReleaseId 2009。

微软宣布 ReleaseId 已弃用,不能再用于正确识别 Windows 10 版本。这对于脚本以及 WSUS、WAC 等工具可能很重要。例如,像 Get-WindowsImage 这样的 cmdlet 现在很难识别正确的 Windows 10 版本。

PowerShell 技能连载 - 原版 Windows 10 产品密钥

有大量 PowerShell 脚本到处流传,它们生成可以解码原始的 Windows 10 产品密钥。大多数这些脚本使用过时的算法,这些算法不再适用于 Windows 10。

这是检索原始 Windows 10 产品密钥的更简单的方法:

1
2
Get-CimInstance -ClassName SoftwareLicensingService |
Select-Object -ExpandProperty OA3xOriginalProductKey

如果该命令不产生任何返回信息,则操作系统安装中没有存储单独的产品密钥。

PowerShell 技能连载 - 评估事件日志数据(第 3 部分)

在上一个技能中,我们了解了 Get-WinEvent 以及如何使用计算属性直接访问附加到每个事件的“属性”,而不必对事件消息进行文本解析。

例如,下面的代码通过从“属性”中找到的数组中提取已安装更新的名称来生成已安装更新列表:

1
2
3
4
$software = @{
Name = 'Software' Expression = { $_.Properties[0].Value }
}Get-WinEvent -FilterHashTable @{
Logname='System' ID=19 ProviderName='Microsoft-Windows-WindowsUpdateClient'} | Select-Object -Property TimeCreated, $software

这个概念一般适用于所有事件类型,您唯一的工作就是找出哪些信息包含在哪个数组索引中。让我们来看一个更复杂的事件类型,它包含的不仅仅是一条信息:

1
2
3
4
5
6
7
8
9
10
11
12
$LogonType = @{
Name = 'LogonType' Expression = { $_.Properties[8].Value }
}$Process = @{
Name = 'Process' Expression = { $_.Properties[9].Value }
}$Domain = @{
Name = 'Domain' Expression = { $_.Properties[5].Value }
}$User = @{
Name = 'User' Expression = { $_.Properties[6].Value }
}$Method = @{
Name = 'Method' Expression = { $_.Properties[10].Value }
}Get-WinEvent -FilterHashtable @{
LogName = 'Security' Id = 4624 } | Select-Object -Property TimeCreated, $LogonType, $Process, $Domain, $User, $Method

在这里,Get-WinEvent 从安全日志中读取 ID 为 4624 的所有事件。这些事件代表登录。由于事件位于安全日志中,因此您需要本地管理员权限才能运行代码。

Select-Object 仅返回 TimeCreated 属性。所有剩余的属性都被计算出来,基本上都是一样的:它们从所有事件日志条目对象中找到的“属性”数组中提取一些信息。

事实证明,登录的用户名可以在该数组的索引 6 中找到,登录类型可以在数组索引 8 中找到。

将代码包装到一个函数中后,现在可以很容易地对记录的登录事件进行复杂的查询:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function Get-LogonInfo{
$LogonType = @{
Name = 'LogonType' Expression = { $_.Properties[8].Value }
}

$Process = @{
Name = 'Process' Expression = { $_.Properties[9].Value }
}

$Domain = @{
Name = 'Domain' Expression = { $_.Properties[5].Value }
}

$User = @{
Name = 'User' Expression = { $_.Properties[6].Value }
}

$Method = @{
Name = 'Method' Expression = { $_.Properties[10].Value }
}

Get-WinEvent -FilterHashtable @{
LogName = 'Security' Id = 4624 } | Select-Object -Property TimeCreated, $LogonType, $Process, $Domain, $User, $Method}Get-LogonInfo | Where-Object Domain -ne System | Where-Object User -ne 'Window Manager' | Select-Object -Property TimeCreated, Domain, User, Method

结果类似于:

TimeCreated         Domain                  User             Method
-----------         ------                  ----             ------
06.05.2021 11:46:04 RemotingUser2           DELL7390         Negotiate
05.05.2021 19:20:16 tobi.weltner@-------.de MicrosoftAccount Negotiate
05.05.2021 19:20:06 UMFD-1                  Font Driver Host Negotiate
05.05.2021 19:20:05 UMFD-0                  Font Driver Host Negotiate
PowerShell 技术 QQ 群