PowerShell 技能连载 - 响应新的事件日志条目(第 2 部分)

以下是另一个文件系统任务,听起来复杂,实际并没有那么复杂。假设您需要移除一个文件夹结构中指定层次之下的所有文件夹。以下是实现方法:

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
# set the event log name you want to subscribe to
# (use Get-EventLog -AsString for a list of available event log names)
$Name = 'Application'

# get an instance
$Log = [System.Diagnostics.EventLog]$Name

# determine what to do when an event occurs
$Action = {
# get the original event entry that triggered the event
$entry = $event.SourceEventArgs.Entry

# log all events
Write-Host "Received from $($entry.Source): $($entry.Message)"

# do something based on a specific event
if ($entry.EventId -eq 1 -and $entry.Source -eq 'WinLogon')
{
Write-Host "Test event was received!" -ForegroundColor Red
}

}

# subscribe to its "EntryWritten" event
$job = Register-ObjectEvent -InputObject $log -EventName EntryWritten -SourceIdentifier 'NewEventHandler' -Action $Action

# now whenever an event is written to the log, $Action is executed
# use a loop to keep PowerShell busy. You can abort via CTRL+C

Write-Host "Listening to events" -NoNewline

try
{
do
{
Wait-Event -SourceIdentifier NewEventHandler -Timeout 1
Write-Host "." -NoNewline

} while ($true)
}
finally
{
# this executes when CTRL+C is pressed
Unregister-Event -SourceIdentifier NewEventHandler
Remove-Job -Name NewEventHandler
Write-Host ""
Write-Host "Event handler stopped."
}

由于事件处理器是活跃的,PowerShell 每秒输出一个“点”,表示它仍在监听。现在打开另一个 PowerShell 窗口,并且运行以下代码:

1
Write-EventLog -LogName Application -Source WinLogon -EntryType Information -Message test -EventId 1

当写入一个新的应用程序事件日志条目时,事件处理器显示事件的详情。如果事件的 EventID 等于 1 并且事件源为 “WinLogon“,例如我们的测试事件条目,那么将会输出一条红色的信息。

要停止事件监听器,请按 CTRL+C。代码将自动清理并从内存中移除事件监听器。

这都是通过 Wait-Event 实现的:这个 cmdlet 可以等待特定的事件发生。并且当它等待时,PowerShell 可以继续执行事件处理器。当您指定一个超时(以秒为单位),该 cmdlet 将控制权返回给脚本。在我们的栗子中,控制权每秒钟都会返回,使脚本有机会以点的方式输出进度指示器。

如果用户按下 CTRL+C,该脚本并不会立即停止。相反,它会先执行 finally 语句块并确保该事件处理器已清除和移除。

PowerShell 技能连载 - 响应新的事件日志条目(第 1 部分)

如果您希望实时处理新的事件日志条目,以下是当新的事件条目写入时,PowerShell 代码能自动收到通知的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# set the event log name you want to subscribe to
# (use Get-EventLog -AsString for a list of available event log names)
$Name = 'Application'

# get an instance
$Log = [System.Diagnostics.EventLog]$Name

# determine what to do when an event occurs
$Action = {
# get the original event entry that triggered the event
$entry = $event.SourceEventArgs.Entry

# do something based on the event
if ($entry.EventId -eq 1 -and $entry.Source -eq 'WinLogon')
{
Write-Host "Test event was received!"
}

}

# subscribe to its "EntryWritten" event
$job = Register-ObjectEvent -InputObject $log -EventName EntryWritten -SourceIdentifier 'NewEventHandler' -Action $Action

这段代码片段安装了一个后台的事件监听器,每当事件日志产生一个 “EntryWritten” 事件时会触发。当发生该事件时,$Action 中的代码就会执行。它通过查询 $event 变量获取触发该操作的事件。在我们的示例中,当 EventId 等于 1,并且事件源是 “WinLogon“ 时,会写入一条信息。当然,您可以发送一封邮件,写入一条日志,或做任何其它有用的事情。

要查看事件处理器的效果,只需要写一个符合所有要素的测试事件:

1
2
# write a fake test event to trigger
Write-EventLog -LogName Application -Source WinLogon -EntryType Information -Message test -EventId 1

当您运行这段代码时,事件处理器将自动执行并向控制台输出自己的信息。

请注意这个例子将安装一个异步的处理器,并且当 PowerShell 不忙碌的时候会在后台执行,并且在 PowerShell 运行期间都有效。您不能通过 Start-Sleep 或一个循环来保持 PowerShell 忙碌(因为 PowerShell 会忙碌而无法在后台处理事件处理器)。要保持这个事件处理器可响应,您可以通过 -noexit 参数启动脚本:

1
Powershell.exe -noprofile -noexit -file “c:\yourpath.ps1”

要移除事件处理器,请运行以下代码:

1
2
PS> Unregister-Event -SourceIdentifier NewEventHandler
PS> Remove-Job -Name NewEventHandler

PowerShell 技能连载 - 直接存取事件日志

通过 Get-EventLog 命令,您可以轻松地转储任意给定的事件日志内容,然而,如果您希望直接存取某个指定的事件日志,您只能使用 -List 参数来完全转储它们,然后选择其中一个您所关注的:

1
2
$SystemLog = Get-EventLog -List | Where-Object { $_.Log -eq 'System' }
$SystemLog

一个更简单的方式是使用强制类型转换,类似这样:

1
2
$systemLogDirect = [System.Diagnostics.EventLog]'System'
$systemLogDirect

Simply “convert” the event log name into an object of “EventLog” type. The result looks similar to this and provides information about the number of entries and the log file size:
只需要将时间日志名称“转换”为一个 “EventLog“ 类型的对象。结果类似这样,并且提供了条目的数量和日志文件尺寸等信息:

1
2
3
4
5
PS> $systemLogDirect

Max(K) Retain OverflowAction Entries Log
------ ------ -------------- ------- ---
20.480 0 OverwriteAsNeeded 19.806 System

PowerShell 技能连载 - 使用一个停表

在 PowerShell 中,要测量时间,您可以简单将一个 datetime 值减去另一个 datetime 值:

1
2
3
4
5
6
7
$Start = Get-Date

$null = Read-Host -Prompt "Press ENTER as fast as you can!"

$Stop = Get-Date
$TimeSpan = $Stop - $Start
$TimeSpan.TotalMilliseconds

一个优雅的实现是用停表:

1
2
3
4
5
6
$StopWatch = [Diagnostics.Stopwatch]::StartNew()

$null = Read-Host -Prompt "Press ENTER as fast as you can!"

$StopWatch.Stop()
$StopWatch.ElapsedMilliseconds

使用停表的好处是可以暂停和继续。

PowerShell 技能连载 - 使用 $MyInvocation 的固定替代方式

类似 $MyInvocation.MyCommand.Definition 的代码对于确定当前脚本存储的位置十分有用,例如需要存取同一个文件夹下其它资源的时候。

然而,从 PowerShell 3 开始,有一个更简单的替代方式可以查找当前脚本的名称和/或当前脚本文件夹的路径。请自己运行以下代码测试:

1
2
3
4
5
$MyInvocation.MyCommand.Definition
$PSCommandPath

Split-Path -Path $MyInvocation.MyCommand.Definition
$PSScriptRoot

如果您交互式运行这段代码(或在一个“无标题”脚本中),它们都不会返回任何内容。但是当您将脚本保存后执行,这两行代码将返回脚本的路径,并且后两行代码将返回脚本所在文件夹的路径。

$PSCommandPath$PSScriptRoot 的好处在于它们总是包含相同的信息。相比之下,$MyInvocation 可能会改变,而且当从一个函数中读取这个变量时,它就会发生改变。

1
2
3
4
5
6
7
8
9
10
function test
{
$MyInvocation.MyCommand.Definition
$PSCommandPath

Split-Path -Path $MyInvocation.MyCommand.Definition
$PSScriptRoot
}

test

现在,$MyInvocation 变得没有价值,因为它重视返回调用本脚本块的调用者信息。

PowerShell 技能连载 - 查找打开的防火墙端口

以下是一段连接到本地防火墙并转储所有打开的防火墙端口的 PowerShell 代码:

1
2
3
$firewall = New-object -ComObject HNetCfg.FwPolicy2
$firewall.Rules | Where-Object {$_.Action -eq 0} |
Select-Object Name, ApplicationName,LocalPorts

结果看起来类似这样:

Name           ApplicationName                                         LocalPorts
----           ---------------                                         ----------
pluginhost.exe C:\users\tobwe\appdata\local\skypeplugin\pluginhost.exe *
pluginhost.exe C:\users\tobwe\appdata\local\skypeplugin\pluginhost.exe *
spotify.exe    C:\users\tobwe\appdata\roaming\spotify\spotify.exe      *
spotify.exe    C:\users\tobwe\appdata\roaming\spotify\spotify.exe      *

在 Windows 10 和 Server 2016 中,有一系列现成的跟防火墙有关的 cmdlet:

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
PS> Get-Command -Noun *Firewall*

CommandType Name Version Source
----------- ---- ------- ------
Function Copy-NetFirewallRule 2.0.0.0 NetSecurity
Function Disable-NetFirewallRule 2.0.0.0 NetSecurity
Function Enable-NetFirewallRule 2.0.0.0 NetSecurity
Function Get-NetFirewallAddressFilter 2.0.0.0 NetSecurity
Function Get-NetFirewallApplicationFilter 2.0.0.0 NetSecurity
Function Get-NetFirewallInterfaceFilter 2.0.0.0 NetSecurity
Function Get-NetFirewallInterfaceTypeFilter 2.0.0.0 NetSecurity
Function Get-NetFirewallPortFilter 2.0.0.0 NetSecurity
Function Get-NetFirewallProfile 2.0.0.0 NetSecurity
Function Get-NetFirewallRule 2.0.0.0 NetSecurity
Function Get-NetFirewallSecurityFilter 2.0.0.0 NetSecurity
Function Get-NetFirewallServiceFilter 2.0.0.0 NetSecurity
Function Get-NetFirewallSetting 2.0.0.0 NetSecurity
Function New-NetFirewallRule 2.0.0.0 NetSecurity
Function Remove-NetFirewallRule 2.0.0.0 NetSecurity
Function Rename-NetFirewallRule 2.0.0.0 NetSecurity
Function Set-NetFirewallAddressFilter 2.0.0.0 NetSecurity
Function Set-NetFirewallApplicationFilter 2.0.0.0 NetSecurity
Function Set-NetFirewallInterfaceFilter 2.0.0.0 NetSecurity
Function Set-NetFirewallInterfaceTypeFilter 2.0.0.0 NetSecurity
Function Set-NetFirewallPortFilter 2.0.0.0 NetSecurity
Function Set-NetFirewallProfile 2.0.0.0 NetSecurity
Function Set-NetFirewallRule 2.0.0.0 NetSecurity
Function Set-NetFirewallSecurityFilter 2.0.0.0 NetSecurity
Function Set-NetFirewallServiceFilter 2.0.0.0 NetSecurity
Function Set-NetFirewallSetting 2.0.0.0 NetSecurity
Function Show-NetFirewallRule 2.0.0.0 NetSecurity

PowerShell 技能连载 - 为代码执行添加超时(第 2 部分)

在前一个技能中我们通过 PowerShell 后台作业实现了超时机制,这样您可以设置某段代码执行的最大允许时间,超过该时间将抛出一个异常。

以下是一个更轻量级的替代,它使用进程内线程,而不是进程外执行:

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
function Invoke-CodeWithTimeout
{
param
(
[Parameter(Mandatory)]
[ScriptBlock]
$Code,

[int]
$Timeout = 5

)

$ps = [PowerShell]::Create()
$null = $ps.AddScript($Code)
$handle = $ps.BeginInvoke()
$start = Get-Date
do
{
$timeConsumed = (Get-Date) - $start
if ($timeConsumed.TotalSeconds -ge $Timeout) {
$ps.Stop()
$ps.Dispose()
throw "Job timed out."
}
Start-Sleep -Milliseconds 300
} until ($handle.isCompleted)

$ps.EndInvoke($handle)
$ps.Runspace.Close()
$ps.Dispose()
}

以下是使用该新超时机制的方法:

1
2
3
4
5
6
7
8
9
10
11
12
PS> Invoke-CodeWithTimeout -Code { Start-Sleep -Seconds 6; Get-Date } -Timeout 5
Job timed out.
At line:24 char:13
+ throw "Job timed out."
+ ~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : OperationStopped: (Job timed out.:String) [], RuntimeException
+ FullyQualifiedErrorId : Job timed out.


PS> Invoke-CodeWithTimeout -Code { Start-Sleep -Seconds 3; Get-Date } -Timeout 5

Thursday November 1, 2018 14:53:26

PowerShell 技能连载 - 为代码执行添加超时(第 1 部分)

如果希望某些代码不会无限执行下去,您可以使用后台任务来实现超时机制。以下是一个示例函数:

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
function Invoke-CodeWithTimeout
{
param
(
[Parameter(Mandatory)]
[ScriptBlock]
$Code,

[int]
$Timeout = 5

)

$j = Start-Job -ScriptBlock $Code
$completed = Wait-Job $j -Timeout $Timeout
if ($completed -eq $null)
{
throw "Job timed out."
Stop-Job -Job $j
}
else
{
Receive-Job -Job $j
}
Remove-Job -Job $j
}

所以基本上说,要让代码执行的时间不超过 5 秒,请试试以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
PS> Invoke-CodeWithTimeout -Code { Start-Sleep -Seconds 6; Get-Date } -Timeout 5
Job timed out.
At line:18 char:7
+ throw "Job timed out."
+ ~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : OperationStopped: (Job timed out.:String) [], RuntimeException
+ FullyQualifiedErrorId : Job timed out.


PS> Invoke-CodeWithTimeout -Code { Start-Sleep -Seconds 3; Get-Date } -Timeout 5

Thursday November 1, 2018 14:53:26

该方法有效。但是,它所使用的作业相关的开销相当大。创建后台作业并将数据返回到前台任务的开销可能增加了额外的时间。所以我们将在明天的技能中寻求一个更好的方法。

PowerShell 技能连载 - 代码签名迷你系列(第 5 部分:审计签名)

当 Powershell 脚本携带一个数字签名后,您可以快速地找出是谁对这个脚本签的名,更重要的是,这个脚本是否未被篡改。在本系列之前的部分中,您学习了如何创建数字签名,以及如何对 PowerShell 文件签名。现在我们来看看如何验证脚本的合法性。

1
2
3
4
5
# this is the path to the scripts you'd like to examine
$Path = "$home\Documents"

Get-ChildItem -Path $Path -Filter *.ps1 -Recurse |
Get-AuthenticodeSignature

只需要调整路径,该脚本讲查找该路径下的所有 PowerShell 脚本,然后检查它们的签名。结果有如下可能性:

NotSigned:	没有签名
UnknownError:	使用非受信的证书签名
HashMismatch:	签名之后修改过
Valid:		采用受信任的证书签名,并且没有改动过

PowerShell 技能连载 - 代码签名迷你系列(第 4 部分:签名 PowerShell 文件)

在将 PowerShell 脚本发送给别人之前,最好对它进行数字签名。签名的角色类似脚本的“包装器”,可以帮助别人确认是谁编写了这个脚本以及这个脚本是否仍是原始本版本,或是已被篡改过。

要对 PowerShell 脚本签名,您需要一个数字代码签名证书。在前一个技能中我们解释了如何创建一个该证书,并且/或者从 pfx 或证书存储中加载该证书。以下代码假设在 $cert 中已经有了一个合法的代码签名证书。如果还没有,请先阅读之前的技能文章!

1
2
3
4
5
6
7
8
9
10
11
12
13
# make sure this PFX file exists or create one
# or load a code-signing cert from other sources
# (review the previous tips for hints)
$pfxFile = "$home\desktop\tobias.pfx"
$cert = Get-PfxCertificate -FilePath $pfxFile

# make sure this folder exists and contains
# PowerShell script that you'd like to sign
$PathWithScripts = 'c:\myScripts'

# apply signatures to all scripts in the folder
Get-ChildItem -Path $PathWithScripts -Filter *.ps1 -Recurse |
Set-AuthenticodeSignature -Certificate $cert

运行这段代码后,指定目录中的所有脚本都会添加上数字签名。如果您连接到了 Internet,应该考虑签名时使用时间戳服务器,并且将最后一行代码替换成这行:

1
2
3
# apply signatures to all scripts in the folder
Get-ChildItem -Path $PathWithScripts -Filter *.ps1 -Recurse |
Set-AuthenticodeSignature -Certificate $cert -TimestampServer http://timestamp.digicert.com

使用时间戳服务器会减慢签名的速度但是确保不会用过期的证书签名:例如当某天一本证书过期了,但是签名仍然有效。因为官方的时间戳服务器,签名仍然有效,因为官方的时间戳服务器证明该签名是在证书过期之前应用的。