PowerShell 技能连载 - 压缩序列化的数据

通过 Export-CliXml 命令可以很方便地将处理结果保存到文件通过 Import-CliXml 命令,序列化的信息可以同样方便地恢复。然而,生成的 XML 文件可能非常大。

幸运的是,在 PowerShell 5 中有一个新的命令名叫 Compress-Archieve。当您创建了 XML 文件之后,您可以自动地将它转为一个 ZIP 文件。

以下是一些演示代码:它获取一个进程列表并且保存到 XML 文件。然后将 XML 文件压缩为 ZIP,并且删除原始的 XML 文件。

这么做的效率很高,因为 XML 是文本文件。您常常能看到压缩率在 3-5%(ZIP 的文件大小是原始文件的 3-5%):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$Path = "$env:TEMP\data1.xml"

# serialize data (results in large text files)
Get-Process | Export-Clixml -Path $Path

$length1 = (Get-Item -Path $Path).Length

$ZipPath = [IO.Path]::ChangeExtension($Path, ".zip")

Compress-Archive -Path $Path -Destination $ZipPath -CompressionLevel Optimal -Force
Remove-Item -Path $Path

$length2 = (Get-Item -Path $ZipPath).Length

$compression = $length2 * 100 / $length1
"Compression Ratio {0:n2} %" -f $compression

PowerShell 技能连载 - Multipass: 安全存储多个凭据

If you’d like to safely store credentials (usernames and password) for your personal use in a file, here is a very simple yet extremely powerful approach. Take a look at this code:

$Path = "$home\Desktop\multipass.xml"

[PSCustomObject]@{
    User1 = Get-Credential -Message User1
    User2 = Get-Credential -Message User2
    User3 = Get-Credential -Message User3
} | Export-Clixml -Path $Path

When you run it, it asks for three credentials and saves them to a “multipass” file on your desktop. All passwords are safely encrypted with your identity and your machines identity (which is why the file can only be read by you, and only on the machine where it was created).

To later on use one of the credentials, this is how you read them back in:

$multipass = Import-Clixml -Path $Path

You can then access the credentials via the properties “User1”, “User2”, and “User3”, and use the credentials in your scripts wherever a cmdlet asks for a credential:

PS C:\> $multipass.User1

UserName                     Password
--------                     --------
AlbertK  System.Security.SecureString

PowerShell 技能连载 - 一个更好(更快)的 Start-Job

Start-Job 将一个脚本块发送到一个新的 PowerShell 进程,这样它可以独立并行运行。以下是一个非常简单的例子,演示 job 背后的概念:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# three separate "jobs" to do:
$job1 = { Start-Sleep -Seconds 6 ; 1 }
$job2 = { Start-Sleep -Seconds 8 ; 2 }
$job3 = { Start-Sleep -Seconds 5 ; 3 }

# execute two of them in background jobs
$j1 = Start-Job -ScriptBlock $job1
$j3 = Start-Job -ScriptBlock $job3

# execute one in our own process
$ej2 = & $job2

# wait for all to complete
$null = Wait-Job -Job $J1, $j3

# get the results and clean up
$ej1 = Receive-Job -Job $j1
$ej3 = Receive-Job -Job $j3
Remove-Job -Job $j1, $j3

# work with the results
$ej1, $ej2, $ej3

如果不用 job,那么需要等待 19 秒。幸好有了 job,这个过程可以缩短到 8 秒。

然而,也有副作用。由于 job 是在独立的应用中执行的,数据必须以 XML 序列化的方式来回传递。job 要传回越多的数据,就需要越多的时间。有些时候这个副作用会盖过了优点。

一个更好的方是在原来的 PowerShell 实例的子线程中运行 job。以下代码演示这种功能。它创建了一个新的名为 Start-MemoryJob 的命令,可以替代 Start-Job。其余的代码完全不用改变。

使用 Start-MemoryJob,不需要任何对象序列化。您的 job 可以快速平滑地运行,而没有返回大量的数据。而且,您现在获取到的是原始的对象。不再需要处理序列化过的对象。

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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
$code = @'
using System;
using System.Collections.Generic;
using System.Text;
using System.Management.Automation;
using System.Management.Automation.Runspaces;
namespace InProcess
{
public class InMemoryJob : System.Management.Automation.Job
{
public InMemoryJob(ScriptBlock scriptBlock, string name)
{
_PowerShell = PowerShell.Create().AddScript(scriptBlock.ToString());
SetUpStreams(name);
}
public InMemoryJob(PowerShell PowerShell, string name)
{
_PowerShell = PowerShell;
SetUpStreams(name);
}
private void SetUpStreams(string name)
{
_PowerShell.Streams.Verbose = this.Verbose;
_PowerShell.Streams.Error = this.Error;
_PowerShell.Streams.Debug = this.Debug;
_PowerShell.Streams.Warning = this.Warning;
_PowerShell.Runspace.AvailabilityChanged +=
new EventHandler<RunspaceAvailabilityEventArgs>(Runspace_AvailabilityChanged);
int id = System.Threading.Interlocked.Add(ref InMemoryJobNumber, 1);
if (!string.IsNullOrEmpty(name))
{
this.Name = name;
}
else
{
this.Name = "InProcessJob" + id;
}
}
void Runspace_AvailabilityChanged(object sender, RunspaceAvailabilityEventArgs e)
{
if (e.RunspaceAvailability == RunspaceAvailability.Available)
{
this.SetJobState(JobState.Completed);
}
}
PowerShell _PowerShell;
static int InMemoryJobNumber = 0;
public override bool HasMoreData
{
get {
return (Output.Count > 0);
}
}
public override string Location
{
get { return "In Process"; }
}
public override string StatusMessage
{
get { return "A new status message"; }
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
if (!isDisposed)
{
isDisposed = true;
try
{
if (!IsFinishedState(JobStateInfo.State))
{
StopJob();
}
foreach (Job job in ChildJobs)
{
job.Dispose();
}
}
finally
{
base.Dispose(disposing);
}
}
}
}
private bool isDisposed = false;
internal bool IsFinishedState(JobState state)
{
return (state == JobState.Completed || state == JobState.Failed || state ==
JobState.Stopped);
}
public override void StopJob()
{
_PowerShell.Stop();
_PowerShell.EndInvoke(_asyncResult);
SetJobState(JobState.Stopped);
}
public void Start()
{
_asyncResult = _PowerShell.BeginInvoke<PSObject, PSObject>(null, Output);
SetJobState(JobState.Running);
}
IAsyncResult _asyncResult;
public void WaitJob()
{
_asyncResult.AsyncWaitHandle.WaitOne();
}
public void WaitJob(TimeSpan timeout)
{
_asyncResult.AsyncWaitHandle.WaitOne(timeout);
}
}
}
'@
Add-Type -TypeDefinition $code
function Start-JobInProcess
{
[CmdletBinding()]
param
(
[scriptblock] $ScriptBlock,
$ArgumentList,
[string] $Name
)
function Get-JobRepository
{
[cmdletbinding()]
param()
$pscmdlet.JobRepository
}
function Add-Job
{
[cmdletbinding()]
param
(
$job
)
$pscmdlet.JobRepository.Add($job)
}
if ($ArgumentList)
{
$PowerShell = [PowerShell]::Create().AddScript($ScriptBlock).AddArgument($argumentlist)
$MemoryJob = New-Object InProcess.InMemoryJob $PowerShell, $Name
}
else
{
$MemoryJob = New-Object InProcess.InMemoryJob $ScriptBlock, $Name
}
$MemoryJob.Start()
Add-Job $MemoryJob
$MemoryJob
}

PowerShell 技能连载 - 结合时间戳服务器使用数字签名

当您对脚本文件签名时,会希望签名保持完整,即便为它签名的证书将来过期了。关键的是证书在签名的时候是合法的。

要确保这一点,您需要一个受信的机构提供的时间戳服务器。通过这种方式,您不仅是对一个脚本签名,而且添加了签名的时间。在证书有效期内,一切没有问题。

我们调整了前一个技能中的代码,并且添加了一个时间戳服务器的 URL。以下代码对用户数据文件中所有未签名的脚本文件增加签名。如果您确定要对您的脚本文件增加签名,请移除 -WhatIf 参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
# read in the certificate from a pre-existing PFX file
$cert = Get-PfxCertificate -FilePath "$env:temp\codeSignCert.pfx"

# find all scripts in your user profile...
Get-ChildItem -Path $home\Documents -Filter *.ps1 -Include *.ps1 -Recurse -ErrorAction SilentlyContinue |
# ...that do not have a signature yet...
Where-Object {
($_ | Get-AuthenticodeSignature).Status -eq 'NotSigned'
} |
# and apply one
# (note that we added -WhatIf so no signing occurs. Remove this only if you
# really want to add digital signatures!)
Set-AuthenticodeSignature -Certificate $cert -TimestampServer http://timestamp.digicert.com -WhatIf

PowerShell 技能连载 - 对 PowerShell 脚本进行数字签名

在前一个技能中您学习了如何创建一个自签名的代码签名证书、将证书保存为一个 PFX 文件,并且将它加载进内存。

今天,假设您已经有了一个包含代码签名证书的 PFX 文件,我们将看看如何对 PowerShell 脚本进行数字签名。

以下代码在您的用户配置文件中查找所有 PowerShell 脚本,如果脚本还未经过数字签名,将会从 PFX 文件中读取一个证书对它进行签名:

1
2
3
4
5
6
7
8
9
10
11
12
13
# read in the certificate from a pre-existing PFX file
$cert = Get-PfxCertificate -FilePath "$env:temp\codeSignCert.pfx"

# find all scripts in your user profile...
Get-ChildItem -Path $home -Filter *.ps1 -Include *.ps1 -Recurse -ErrorAction SilentlyContinue |
# ...that do not have a signature yet...
Where-Object {
($_ | Get-AuthenticodeSignature).Status -eq 'NotSigned'
} |
# and apply one
# (note that we added -WhatIf so no signing occurs. Remove this only if you
# really want to add digital signatures!)
Set-AuthenticodeSignature -Certificate $cert -WhatIf

当您在编辑器中查看这些脚本时,您将在脚本的底部看到一个新的注释段。它包含了使用证书加密的脚本的哈希值。他也包含了证书的公开信息。

当您右键点击一个签名的脚本并且选择“属性”,可以看到谁对脚本做了签名。如果您确实信任这个人,您可以将它们的证书安装到受信任的根证书中。

一旦脚本经过数字签名,就可以很方便地审查脚本状态。签名可以告诉您谁签名了一个脚本,以及脚本的内容是否被纂改过。以下代码检查用户配置文件中所有的 PowerShell 脚本并显示签名状态:

1
2
3
4
# find all scripts in your user profile...
Get-ChildItem -Path $home -Filter *.ps1 -Include *.ps1 -Recurse -ErrorAction SilentlyContinue |
# ...and check signature status
Get-AuthenticodeSignature

如果您收到“UnknownError”消息,这并不代表是一个未知错误,而是代表脚本未受纂改但签名在系统中是未知的(或非受信的)。

PowerShell 技能连载 - 从 PFX 文件加载证书

在前一个技能中我们演示了如何使用 New-SelfSignedCertificate 来创建新的代码签名证书,并且将它们存储为一个 PFX 文件。今天让我们来看看如何加载一个 PFX 文件。

假设您的 PFX 文件存放在 $env:temp\codeSignCert.pfx。以下是读取该文件的代码:

1
$cert = Get-PfxCertificate -FilePath "$env:temp\codeSignCert.pfx"

这段代码执行时,将会提示输入密码。这个密码是您创建证书时输入的密码,并且它保护这个文件不被滥用。

当命令成功执行以后,可以从 $cert 变量获取证书详细信息:

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
PS C:\> $cert

Thumbprint Subject
---------- -------
5D8A325641CC583F882B439833961AE9BCDEC946 CN=SecurityDepartment



PS C:\> $cert | Select-Object -Property *


EnhancedKeyUsageList : {Code Signing (1.3.6.1.5.5.7.3.3)}
DnsNameList : {SecurityDepartment}
SendAsTrustedIssuer : False
EnrollmentPolicyEndPoint : Microsoft.CertificateServices.Commands.EnrollmentEndPointProperty
EnrollmentServerEndPoint : Microsoft.CertificateServices.Commands.EnrollmentEndPointProperty
PolicyId :
Archived : False
Extensions : {System.Security.Cryptography.Oid, System.Security.Cryptography.Oid, System.Security.Cryptography.Oid}
FriendlyName : IT Sec Department
IssuerName : System.Security.Cryptography.X509Certificates.X500DistinguishedName
NotAfter : 9/29/2022 12:57:28 AM
NotBefore : 9/29/2017 12:47:28 AM
HasPrivateKey : True
PrivateKey : System.Security.Cryptography.RSACryptoServiceProvider
PublicKey : System.Security.Cryptography.X509Certificates.PublicKey
RawData : {48, 130, 3, 10...}
SerialNumber : 45C8C7871DC392A44AD1ADD28FFDFAC7
SubjectName : System.Security.Cryptography.X509Certificates.X500DistinguishedName
SignatureAlgorithm : System.Security.Cryptography.Oid
Thumbprint : 5D8A325641CC583F882B439833961AE9BCDEC946
Version : 3
Handle : 2832940980736
Issuer : CN=SecurityDepartment
Subject : CN=SecurityDepartment

证书对象包含了一系列方法:

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
PS C:\> $cert | Get-Member -MemberType *Method


TypeName: System.Security.Cryptography.X509Certificates.X509Certificate2

Name MemberType Definition
---- ---------- ----------
Dispose Method void Dispose(), void IDisposable.Dispose()
Equals Method bool Equals(System.Object obj), bool Equals(X509Certificate other)
Export Method byte[] Export(System.Security.Cryptography.X509Certificates.X509ContentType contentType), byte[] Export(System.Sec...
GetCertHash Method byte[] GetCertHash()
GetCertHashString Method string GetCertHashString()
GetEffectiveDateString Method string GetEffectiveDateString()
GetExpirationDateString Method string GetExpirationDateString()
GetFormat Method string GetFormat()
GetHashCode Method int GetHashCode()
GetIssuerName Method string GetIssuerName()
GetKeyAlgorithm Method string GetKeyAlgorithm()
GetKeyAlgorithmParameters Method byte[] GetKeyAlgorithmParameters()
GetKeyAlgorithmParametersString Method string GetKeyAlgorithmParametersString()
GetName Method string GetName()
GetNameInfo Method string GetNameInfo(System.Security.Cryptography.X509Certificates.X509NameType nameType, bool forIssuer)
GetObjectData Method void ISerializable.GetObjectData(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization...
GetPublicKey Method byte[] GetPublicKey()
GetPublicKeyString Method string GetPublicKeyString()
GetRawCertData Method byte[] GetRawCertData()
GetRawCertDataString Method string GetRawCertDataString()
GetSerialNumber Method byte[] GetSerialNumber()
GetSerialNumberString Method string GetSerialNumberString()
GetType Method type GetType()
Import Method void Import(byte[] rawData), void Import(byte[] rawData, string password, System.Security.Cryptography.X509Certifi...
OnDeserialization Method void IDeserializationCallback.OnDeserialization(System.Object sender)
Reset Method void Reset()
ToString Method string ToString(), string ToString(bool verbose)
Verify Method bool Verify()

例如,如果您想验证证书是否合法,只需要调用 Verify() 方法。结果是一个布尔值,$false 代表证书不被 Windows 信任。

明天,我们将会使用证书对 PowerShell 脚本进行数字签名。

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

如果您想对您的脚本进行数字签名,首先您需要一个包含“代码签名”功能的数字证书。如果只是测试,您可以方便地创建免费的个人自签名证书。不要期望其他人信任这些证书,因为任何人都可以创建它们。这是一种很好的测试驱动代码签名的方法。

从 PowerShell 4 开始,New-SelfSignedCertificate cmdlet 可以创建签名证书。以下代码创建一个包含私钥和公钥的 PFX 文件:

1
2
3
4
5
6
7
8
9
10
11
12
#requires -Version 5

# this is where the cert file will be saved
$Path = "$env:temp\codeSignCert.pfx"

# you'll need this password to load the PFX file later
$Password = Read-Host -Prompt 'Enter new password to protect certificate' -AsSecureString

# create cert, export to file, then delete again
$cert = New-SelfSignedCertificate -KeyUsage DigitalSignature -KeySpec Signature -FriendlyName 'IT Sec Department' -Subject CN=SecurityDepartment -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')
$cert | Export-PfxCertificate -Password $Password -FilePath $Path
$cert | Remove-Item

在接下来的技能里,我们将看一看可以用新创建的证书来做什么。

PowerShell 技能连载 - 在 Active Directory 中使用 LDAP 过滤器

LDAP 过滤器类似 Active Directory 中使用的查询语言。并且如果您安装了 Microsoft 的 RSAT 工具,您可以很方便地用 ActiveDirectory 模块中的 cmdlet 来用 LDAP 过滤器搜索用户、计算机,或其它资源。

以下代码将查找所有无邮箱地址的用户:

1
2
$filter = '(&(objectCategory=person)(objectClass=user)(!mail=*))'
Get-ADUser -LDAPFilter $filter -Prop *

即便您没有 RSAT 工具和指定的 ActiveDirectory cmdlet 的权限,LDAP 过滤器也十分有用:

1
2
3
4
5
6
$filter = '(&(objectCategory=person)(objectClass=user)(!mail=*))'
$searcher = [ADSISearcher]$filter
# search results only
$searcher.FindAll()
# access to directory entry objects with more details
$searcher.FindAll().GetDirectoryEntry() | Select-Object -Property *

PowerShell 技能连载 - 对比从 PowerShell 远程处理中受到的计算机数据

PowerShell 远程处理是一个查询多台计算机的快速方法,因为 PowerShell 远程处理是并行工作的。以下是一个演示一系列有趣技术的真实案例。

目标是从两台计算机中获取正在运行的进程的列表,然后查找区别。

为了速度最快,进程列表是通过 PowerShell 远程处理和 Invoke-Command,并且结果是从两台计算机获得的。

要区分输入的数据,我们使用了 Group-Object。它通过计算机名对数据集分组。结果是一个哈希表,而计算机名是哈希表的键。

下一步,用 Compare-Object 来快速比较两个列表并查找区别:

1
2
3
4
5
6
7
8
9
10
11
12
13
# get data in parallel via PowerShell remoting
# make sure you adjust the computer names
$computer1 = 'server1'
$computer2 = 'server2'
$data = Invoke-Command { Get-Process } -ComputerName $computer1, $computer2

# separate the data per computer
$infos = $data | Group-Object -Property PSComputerName -AsHashTable -AsString

# find differences in running processes
Compare-Object -ReferenceObject $infos.$computer1 -DifferenceObject $infos.$computer2 -Property ProcessName -PassThru |
Sort-Object -Property ProcessName |
Select-Object -Property ProcessName, Id, PSComputerName, SideIndicator

PowerShell 技能连载 - 从文本生成 MD5 哈希

Get-FileHash cmdlet 可以从文件内容生成哈希值。它无法从任意文本生成哈希值。并且只适用于 PowerShell 5 及更高的版本。

以下是一个小的函数,用 .NET Framework 从任意文本生成 MD5 哈希值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Function Get-StringHash
{
param
(
[String] $String,
$HashName = "MD5"
)
$bytes = [System.Text.Encoding]::UTF8.GetBytes($String)
$algorithm = [System.Security.Cryptography.HashAlgorithm]::Create('MD5')
$StringBuilder = New-Object System.Text.StringBuilder

$algorithm.ComputeHash($bytes) |
ForEach-Object {
$null = $StringBuilder.Append($_.ToString("x2"))
}

$StringBuilder.ToString()
}

每段文本都会生成一个唯一(且短小)的哈希值,所以它可以快速地判断文本是否唯一。它也可以用来检查一大段文本是否有变更过。

1
2
3
4
PS C:\> Get-StringHash "Hello World!"
ed076287532e86365e841e92bfc50d8c

PS C:\>