PowerShell 技能连载 - 以固定宽度分割文本

假设您需要以固定宽度分割一段文本。例如,如果您需要一段文本的前 5 个字符,以及剩余的部分,如何实现它?

大多数 PowerShell 用户可能会用类似这样的方法:

1
$text = 'ID12:Here is the text'$prefix = $text.Substring(0,5)$suffix = $text.Substring(5)$prefix$suffix

当然,如果用分割字符,例如 “:”,可以这样操作:

1
$prefix, $suffix = 'ID12:Here is the text' -split ':'$prefix$suffix

然而,这将会吃掉分割字符,并且它会导致超过两个部分。这不是我们要的目标:用固定宽度分割一段文本。而您仍然可以使用 -split 操作符:

1
$prefix, $suffix = 'ID12:Here is the text' -split '(?<=^.{5})'$prefix$suffix

正则表达式结构 “(?<=XXX)“ 称为“向后引用”。”^“ 代表文本的开始,而 “.“ 代表任何字符。如您猜测的那样,”{5}“ 限定该占位符出现的次数,所以基本上这个正则表达式从剩下的文本中分割出前 5 个字符并且返回两部分(假设文本至少 6 个以上字符长度)。

PowerShell 技能连载 - 获取文本的哈希值

在 PowerShell 5(以及 Get-FileHash 之前),要计算字符串和文件的哈希值,您需要借助原生的 .NET 方法。以下是一段为一个字符串创建 MD5 哈希的示例代码:

1
2
3
4
5
6
7
8
9
10
11
$Text = 'this is the text that you want to convert into a hash'

$Provider = New-Object -TypeName Security.Cryptography.MD5CryptoServiceProvider
$Encodiner = New-Object -TypeName Text.UTF8Encoding

$Bytes = $Encodiner.GetBytes($Text)
$hashBytes = $Provider.ComputeHash($Bytes)
$hash = [System.BitConverter]::ToString($hashBytes)

# remove dashes if needed
$hash -replace '-'

如果您需要计算一个文件内容的哈希值,要么使用 Get-Content 来读取文件,或者使用以下代码:

1
2
3
4
5
6
7
8
9
10
11
$Path = "C:\somefile.txt"

# use your current PowerShell host as a sample
$Path = Get-Process -Id $Pid | Select-Object -ExpandProperty Path

$Provider = New-Object -TypeName Security.Cryptography.MD5CryptoServiceProvider
$FileContent = [System.IO.File]::ReadAllBytes($Path)
$hashBytes = $Provider.ComputeHash($FileContent)
$hash = [System.BitConverter]::ToString($HashBytes)

$hash -replace '-'

PowerShell 技能连载 - 从文本创建哈希

哈希是一种唯一确定一段文本而不用暴露原始文本的棒法。哈希被用来确定文本、查找重复的文件内容,以及验证密码。PowerShell 5 以及更高版本甚至提供了一个 cmdlet 来计算文件的哈希值:Get-FileHash

然而,Get-FileHash 不能计算字符串的哈希。没有必要只是为了计算哈希值而将字符串保存到文件。您可以使用所谓的内存流来代替。以下是一段从任何字符串计算哈希值的代码片段:

1
2
3
4
5
6
7
8
$Text = 'this is the text that you want to convert into a hash'

$stream = [IO.MemoryStream]::new([Text.Encoding]::UTF8.GetBytes($Text))
$hash = Get-FileHash -InputStream $stream -Algorithm SHA1
$stream.Close()
$stream.Dispose()

$hash

使用完成后别忘了关闭并释放内存流,防止内存泄漏并释放所有资源。

PowerShell 技能连载 - 美化 Out-GridView 对话框

当您用管道将对象输出到 Out-GridView,该 cmdlet 显示缺省的属性,所以当您用一个网格视图窗口当作选择框时,您可以控制用户可见的内容。以下代码将读取前 10 个 AD 用户输出到网格视图窗口,并且用户可以选择要返回的项。然而,网格视图窗口中显示的数据看起来很丑:

1
2
3
Get-ADUser -ResultSetSize 10 -Filter * |
Out-GridView -Title 'Select-User' -OutputMode Single |
Select-Object -Property *

如果您没有使用 AD 或没有安装 RSAT 工具,以下是使用进程的类似的例子:

1
2
3
4
Get-Process |
Where-Object MainWindowTitle |
Out-GridView -Title 'Which process do you want to kill?' -OutputMode Single |
Stop-Process -WhatIf

如果您使用 Select-Object 来限制显示的属性,这将改变对象的类型,所以当您继续用管道将改变过的对象传给下一级 cmdlet,它们将无法处理返回的对象。

解决方法是保持对象类型不变,而是改变缺省属性。以下是 AD 用户对象的解决方案,在选择对话框中只显示 Name 和 SID:

1
2
3
4
5
6
7
8
9
[string[]]$visible = 'Name', 'SID'
$type = 'DefaultDisplayPropertySet'
[Management.Automation.PSMemberInfo[]]$i =
New-Object System.Management.Automation.PSPropertySet($type,$visible)

Get-ADUser -LDAPFilter '(samaccountname=schul*)' |
Add-Member -MemberType MemberSet -Name PSStandardMembers -Value $i -Force -PassThru |
Out-GridView -Title 'Select-User' -OutputMode Single |
Select-Object -Property *

这是进程选择框的解决方案,显示进程的名称、公司、起始时间,和窗体标题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[string[]]$visible = 'Name', 'Company','StartTime','MainWindowTitle'
$type = 'DefaultDisplayPropertySet'
[Management.Automation.PSMemberInfo[]]$i =
New-Object System.Management.Automation.PSPropertySet($type,$visible)



Get-Process |
Where-Object MainWindowTitle |
Sort-Object -Property Name |
# important: object clone required
Select-Object -Property * |
Add-Member -MemberType MemberSet -Name PSStandardMembers -Value $i -Force -PassThru |
Out-GridView -Title 'Which process do you want to kill?' -OutputMode Single |
Stop-Process -WhatIf

结果发现,进程对象不接受新的 DefaultDisplayPropertySet,所以在这个例子中需要一个完整的克隆,这样您可以用 Select-Object -Property * 将对象输出到管道。由于这不会改变对象类型,所以所有原始属性都被保留下来,下游管道命令能继续起作用,因为管道绑定仍然有效。

PowerShell 技能连载 - 将 PowerShell 输出重定向到 GridView

当在 PowerShell 中输出数据时,它会静默地通过管道输出到 Out-Default 并且最终以文本的方式输出到控制台。如果我们覆盖 Out-Default,就可以改变它的行为,例如将所有 PowerShell 的输出改到一个网格视图窗口。实际中,您甚至可以区别对待正常的输出和错误信息,并且将两者显示在不同的窗口里。

以下是两个函数:Enable-GridOutputDisable-GridOutput。当您运行 Enable-GridOutput 时,它会覆盖 Out-Default 并将常规的输出显示在 “Output” 网格视图窗口,并且将错误信息转换为有用的文本,并将它输出到一个独立的 “Error” 网格视图窗口。

当运行 Disable-GridOutput 后,会去掉覆盖的效果,并且回到缺省的行为:

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
48
49
50
51
52
53
54
55
56
57
58
function Enable-GridOutput
{
function global:Out-Default
{
param
(
[Parameter(ValueFromPipeline=$true)][Object]
$InputObject
)

begin
{
$cmd = $ExecutionContext.InvokeCommand.
GetCommand('Microsoft.PowerShell.Utility\Out-GridView',
[Management.Automation.CommandTypes]::Cmdlet)

$p1 = {& $cmd -Title 'Output' }.
GetSteppablePipeline($myInvocation.CommandOrigin)
$p2 = {& $cmd -Title 'Error' }.
GetSteppablePipeline($myInvocation.CommandOrigin)

$p1.Begin($PSCmdlet)
$p2.Begin($PSCmdlet)
}

process
{
if ($_ -is [Management.Automation.ErrorRecord])
{
$info = $_ | ForEach-Object { [PSCustomObject]@{
Exception = $_.Exception.Message
Reason = $_.CategoryInfo.Reason
Target = $_.CategoryInfo.TargetName
Script = $_.InvocationInfo.ScriptName
Line = $_.InvocationInfo.ScriptLineNumber
Column = $_.InvocationInfo.OffsetInLine
}
}
$p2.Process($info)
}
else
{
$p1.Process($_)
}
}

end
{
$p1.End()
$p2.End()
}
}
}

function Disable-GridOutput
{
Remove-Item -Path function:Out-Default -ErrorAction SilentlyContinue
}

PowerShell 技能连载 - 对比 AD 用户

您是否曾希望对比 ADUser 的属性?假设您安装了 RSAT 工具,您可以用 Get-ADUser 读取每个 AD 用户,但是对比它们的属性不那么容易。

除非使用以下函数:它基本上是将 AD 用户属性分割成独立的对象,这样便可以使用 Compare-Object

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
#requires -Version 3.0 -Modules ActiveDirectory

function Compare-User
{
param
(
[Parameter(Mandatory)][String]
$User1,

[Parameter(Mandatory)][String]
$User2,

[String[]]
$Filter =$null
)


function ConvertTo-Object
{

process
{
$user = $_
$user.PropertyNames | ForEach-Object {
[PSCustomObject]@{
Name = $_
Value = $user.$_
Identity = $user.SamAccountName
}
}
}
}

$l1 = Get-ADUser -Identity $User1 -Properties * | ConvertTo-Object
$l2 = Get-ADUser -Identity $User2 -Properties * | ConvertTo-Object

Compare-Object -Ref $l1 -Dif $l2 -Property Name, Value |
Sort-Object -Property Name |
Where-Object {
$Filter -eq $null -or $_.Name -in $Filter
}
}

以下是输出可能看起来的样子:

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
PS C:\> Compare-User -User1 student1 -User2 administrator

Name Value
---- -----
accountExpires 0
accountExpires 9223372036854775807
badPasswordTime 131977150131836679
badPasswordTime 131986685447368488
CanonicalName CCIE.LAN/Users/Administrator
CanonicalName CCIE.LAN/Users/student1
CN Administrator
CN student1
Created 08.03.2019 10:31:50
Created 02.04.2019 09:13:17
createTimeStamp 08.03.2019 10:31:50
createTimeStamp 02.04.2019 09:13:17
Description Built-in account for administering the computer/domain
Description
DistinguishedName CN=student1,CN=Users,DC=CCIE,DC=LAN
DistinguishedName CN=Administrator,CN=Users,DC=CCIE,DC=LAN
dSCorePropagationData ...2019 10:47:56, 08.03.2019 10:32:47, 01.01.1601 19:12:16}
dSCorePropagationData {02.04.2019 09:15:28, 01.01.1601 01:00:00}
isCriticalSystemObject True
LastBadPasswordAttempt 22.03.2019 08:56:53
LastBadPasswordAttempt 02.04.2019 10:49:04
lastLogon 131986622819726136
lastLogon 131986685566131171
LastLogonDate 02.04.2019 10:34:39
LastLogonDate 02.04.2019 09:04:41
lastLogonTimestamp 131986622819726136
lastLogonTimestamp 131986676794218709
logonCount 177
logonCount 4
logonHours {255, 255, 255, 255...}
MemberOf ...CIE,DC=LAN, CN=Schema Admins,CN=Users,DC=CCIE,DC=LAN...}
MemberOf ...C=CCIE,DC=LAN, CN=Domain Admins,CN=Users,DC=CCIE,DC=LAN}
Modified 03.04.2019 11:26:30
Modified 02.04.2019 09:04:41
modifyTimeStamp 03.04.2019 11:26:30
modifyTimeStamp 02.04.2019 09:04:41
msDS-User-Account-Control-Computed 8388608
msDS-User-Account-Control-Computed 0
Name Administrator
Name student1
ObjectGUID 6f5d7164-33cf-440a-af8c-3e973a1f381a
ObjectGUID ffe12d2d-cfdd-41f6-8268-41c493786f90
objectSid S-1-5-21-2389183542-1750168592-3050041687-500
objectSid S-1-5-21-2389183542-1750168592-3050041687-1128
PasswordExpired True
PasswordExpired False
PasswordLastSet
PasswordLastSet 08.03.2019 09:41:25
pwdLastSet 0
pwdLastSet 131965080857557947
SamAccountName student1
SamAccountName Administrator
SID S-1-5-21-2389183542-1750168592-3050041687-1128
SID S-1-5-21-2389183542-1750168592-3050041687-500
uSNChanged 25764
uSNChanged 24620
uSNCreated 24653
uSNCreated 8196
whenChanged 02.04.2019 09:04:41
whenChanged 03.04.2019 11:26:30
whenCreated 08.03.2019 10:31:50
whenCreated 02.04.2019 09:13:17

还可以只输出需要的属性:

1
2
3
4
5
6
7
8
9
10
PS C:\> Compare-User -User1 student1 -User2 administrator -Filter memberof, lastlogontime, logonCount, Name

Name Value
---- -----
logonCount 177
logonCount 4
MemberOf ...ise Admins,CN=Users,DC=CCIE,DC=LAN, CN=Schema Admins,CN=Users,DC=CCIE,DC=LAN...}
MemberOf ...LAN, CN=Test1,CN=Users,DC=CCIE,DC=LAN, CN=Domain Admins,CN=Users,DC=CCIE,DC=LAN}
Name Administrator
Name student1

PowerShell 技能连载 - 导出和导入代码签名证书

在前面的技能里我们解释了如何在 Windows 10 和 Server 2016(以及更高的版本)中创建自签名的代码签名证书。今天,我们来看看如何导出这些证书,创建一个密码保护的文件,然后在不同的机器上再次使用这些证书。

假设您已经在个人证书存储中创建了一个新的代码签名证书,或者在您的证书存储中有一个来自其它来源的代码签名证书。这段代码会将证书导出为一个 PFX 文件放在桌面上:

1
2
3
4
5
6
7
8
9
# this password is required to be able to load and use the certificate later
$Password = Read-Host -Prompt 'Enter Password' -AsSecureString
# certificate will be exported to this file
$Path = "$Home\Desktop\myCert.pfx"

# certificate must be in your personal certificate store
$cert = Get-ChildItem -Path Cert:\CurrentUser\My -CodeSigningCert |
Out-GridView -Title 'Select Certificate' -OutputMode Single
$cert | Export-PfxCertificate -Password $Password -FilePath $Path

导出的过程中将会让您输入密码。由于代码签名证书是安全相关的,所以将使用密码来加密存储在 PFX 文件中的证书,并且等等加载证书的时候将需要您输入这个密码。

下一步,将在一个网格视图窗口中显示您个人证书存储中所有的代码签名证书。请选择一个您想导出的证书。

当创建了一个 PFX 文件,您可以用这行命令加载:

1
2
$cert = Get-PfxCertificate -FilePath $Path
$cert | Select-Object -Property *

Get-PfxCertificate 将会让您输入创建 PFX 文件时所输入的密码。当证书加载完,您可以执行 Set-AuthenticodeSignature 用它来签名文件。

PowerShell 技能连载 - 创建代码签名证书

Windows 10 和 Serve 2016(以及更高版本)带有一个高级的 New-SelfSignedCert cmdlet,它可以用来创建代码签名证书。通过代码签名证书,您可以对 PowerShell 脚本进行数字签名并使用这个签名来检测用户是否改篡改过脚本内容。

以下是一个用来创建代码签名证书的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function New-CodeSigningCert
{
param
(
[Parameter(Mandatory, Position=0)]
[System.String]
$FriendlyName,

[Parameter(Mandatory, Position=1)]
[System.String]
$Name
)

# Create a certificate:
New-SelfSignedCertificate -KeyUsage DigitalSignature -KeySpec Signature -FriendlyName $FriendlyName -Subject "CN=$Name" -KeyExportPolicy ExportableEncrypted -CertStoreLocation Cert:\CurrentUser\My -NotAfter (Get-Date).AddYears(5) -TextExtension @('2.5.29.37={text}1.3.6.1.5.5.7.3.3')
}

要创建一个新的证书,请运行这行代码:

1
2
3
4
5
6
7
8
PS> New-CodeSigningCert -FriendlyName TobiasWeltner -Name TWeltner


PSParentPath: Microsoft.PowerShell.Security\Certificate::CurrentUser\My

Thumbprint Subject
---------- -------
2350D77A4CACAF17136B94D297DEB1A5E413655D CN=TWeltner

使用新的代码签名证书,您可以对脚本进行数字签名。代码签名证书位于个人证书存储中。要使用它,需要先从存储中读取它:

1
2
$cert = Get-ChildItem -Path Cert:\CurrentUser\My -CodeSigningCert |
Out-GridView -Title 'Select Certificate' -OutputMode Single

要签名一个单独的脚本,请使用这行代码:

1
2
3
$Path = "C:\path\to\your\script.ps1"

Set-AuthenticodeSignature -Certificate $cert -FilePath $Path

如果您希望通过时间戳签名,请使用这行代码:

1
Set-AuthenticodeSignature -Certificate $cert -TimestampServer http://timestamp.digicert.com -FilePath $Path

当签名使用的证书过期以后,时间戳签名仍然有效。

要批量签名多个脚本,请使用 Get-ChildItem 并用管道将文件传送到 Set-AuthenticodeSignature。这行代码将对用户配置文件中的所有 PowerShell 脚本签名:

1
2
Get-ChildItem -Path "$home\Documents" -Filter *.ps1 -Include *.ps1 -Recurse |
Set-AuthenticodeSignature -Certificate $cert

当您得到了签名后的脚本,随时可以使用 Get-AuthenticodeSignature 来检查签名的完整性:

1
2
Get-ChildItem -Path "$home\Documents" -Filter *.ps1 -Include *.ps1 -Recurse |
Get-AuthenticodeSignature

PowerShell 技能连载 - 使用目录文件来维护文件夹完整性

如果您希望确保一个文件夹的内容保持不变,那么可以使用目录文件。目录文件可以列出所有文件夹内容并为文件夹中的每个文件创建哈希。以下是一个例子:

1
2
3
4
5
6
7
# path to folder to create a catalog file for
# (make sure it exists and isn't too large)
$path = "$Home\Desktop"
# path to catalog file to be created
$catPath = "$env:temp\myDesktop.cat"
# create catalog
New-FileCatalog -Path $path -CatalogVersion 2.0 -CatalogFilePath $catPath

根据文件夹的大小,可能需要一些时间来创建目录文件。您无法创建被锁定和在使用中的文件的目录。生成的目录文件是一个二进制文件并且包含目录中所有文件的哈希。

要检查文件夹是否未被该国,您可以使用 Test-FileCatalog 命令:

1
2
3
4
5
6
7
8
PS> Test-FileCatalog -Detailed -Path $path -CatalogFilePath $catPath


Status : Valid
HashAlgorithm : SHA256
CatalogItems : {...}
PathItems : {...}
Signature : System.Management.Automation.Signature

如果文件夹内容和目录相匹配,那么结果状态为 “Valid”。否则,CatalogItems 属性将包含一个文件夹中所有内容的详细列表,以及它们是否变更过的标志。

PowerShell 技能连载 - 查找 PowerShell 命名管道

每个运行 PowerShell 5 以及以上版本的 PowerShell 宿主都会打开一个能被检测到的“命名管道”。以下代码检测这些命名管道并返回暴露这些管道的进程:

1
2
3
4
Get-ChildItem -Path "\\.\pipe\" -Filter '*pshost*' |
ForEach-Object {
Get-Process -Id $_.Name.Split('.')[2]
}

结果看起来类似这样:

Handles  NPM(K)    PM(K)      WS(K)     CPU(s)     Id  SI ProcessName
-------  ------    -----      -----     ------     --  -- -----------
   1204      98   306220      66620      63,30  28644   1 powershell_ise
    525      29    72604      12708       5,64  12188   1 powershell
    741      41   125728     142656      11,52  27144   1 powershell
    835      61    40836      82624       1,44  22412   1 pwsh
    820      49   199680     230632       2,86  26500   1 powershell_ise

这里列出的每个进程都启动了一个 PowerShell 运行空间。您可以使用 Enter-PSHostProcess -Id XXX 来连接到 PowerShell 进程(假设您有本地管理员特权)。