PowerShell 技能连载 - 在 PowerShell 中安全地使用 WMI(第 1 部分)

WMI (Windows Management Instrumentation) 是 Windows 操作系统的一部分并且广泛用于获取计算机系统的信息。PowerShell 之前引入了 Get-WmiObject 命令。在 PowerShell 3 中,增加了一个更现代的 Get-CimInstance 命令。

为了保持向后兼容,Windows PowerShell 始终保留了旧的 Get-WmiObject 命令,许多脚本开发者仍然在使用它而忽略了 Get-CimInstance。现在该是时候停止这个旧习惯因为 PowerShell Core (PowerShell 6, 7) 停止了对 Get-WmiObject 的支持。要确保脚本支持将来的 PowerShell 版本,您需要开始使用 Get-CimInstance 来代替 Get-WmiObject。

这刚开始看起来是微不足道的改变,实际上也确实是。对于简单的数据查询,您可能会通过 Get-CimInstance 来替换 Get-WmiObject:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
PS> Get-WmiObject -Class Win32_BIOS

SMBIOSBIOSVersion : 1.0.9
Manufacturer : Dell Inc.
Name : 1.0.9
SerialNumber : 4ZKM0Z2
Version : DELL - 20170001

PS> Get-CIMInstance -Class Win32_BIOS

SMBIOSBIOSVersion : 1.0.9
Manufacturer : Dell Inc.
Name : 1.0.9
SerialNumber : 4ZKM0Z2
Version : DELL - 20170001

请注意在 Get-CimInstance-Class 参数实际上改名为 -ClassName,但是由于 PowerShell 允许在参数名称唯一的情况下使用缩写,所以您无须改变参数名称。

然而,实际上 Get-WmiObject 和 Get-CimInstance 并不是 100% 兼容,并且您需要知道许多重要的区别,尤其是当您计划改变已有的脚本时。在这个迷你系列中,我们将看到最重要的实际差异。

让我们看看两个命令返回的信息。以下是一个查看返回属性的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# we are comparing this WMI class (feel free to adjust)
$wmiClass = 'Win32_OperatingSystem'

# get information about the WMI class Win32_OperatingSystem with both cmdlets
$a = Get-WmiObject -Class $wmiClass | Select-Object -First 1
$b = Get-CimInstance -ClassName $wmiClass | Select-Object -First 1

# dump the property names and add the property "Origin" so you know
# which property was returned by which command:
$aDetail = $a.PSObject.Properties | Select-Object -Property Name, @{N='Origin';E={'Get-WmiObject'}}
$bDetail = $b.PSObject.Properties | Select-Object -Property Name, @{N='Origin';E={'Get-CimInstance'}}

# compare the results:
Compare-Object -ReferenceObject $aDetail -DifferenceObject $bDetail -Property Name -PassThru |
Sort-Object -Property Origin

以下是执行结果:

Name                  Origin          SideIndicator
----                  ------          -------------
CimClass              Get-CimInstance =>
CimInstanceProperties Get-CimInstance =>
CimSystemProperties   Get-CimInstance =>
Qualifiers            Get-WmiObject   <=
SystemProperties      Get-WmiObject   <=
Properties            Get-WmiObject   <=
ClassPath             Get-WmiObject   <=
Options               Get-WmiObject   <=
Scope                 Get-WmiObject   <=
__PATH                Get-WmiObject   <=
__NAMESPACE           Get-WmiObject   <=
__SERVER              Get-WmiObject   <=
__DERIVATION          Get-WmiObject   <=
__PROPERTY_COUNT      Get-WmiObject   <=
__RELPATH             Get-WmiObject   <=
__DYNASTY             Get-WmiObject   <=
__SUPERCLASS          Get-WmiObject   <=
__CLASS               Get-WmiObject   <=
__GENUS               Get-WmiObject   <=
Site                  Get-WmiObject   <=
Container             Get-WmiObject   <=

结果显示 metadata 中有显著的区别。Get-WmiObject 总是返回再起属性 “__Server”(两个下划线)中进行查询的计算机的名称;而 Get-WmiObject 则是在 CimSystemProperties 中列出此信息:

1
2
3
4
5
6
7
8
PS> $b.CimSystemProperties

Namespace ServerName ClassName Path
--------- ---------- --------- ----
root/cimv2 DESKTOP-8DVNI43 Win32_Process

PS> $b.CimSystemProperties.ServerName
DESKTOP-8DVNI43

不过,好消息是,类的特定属性并没有不同,因此这两个命令都返回有关操作系统、BIOS 或您所查询的其它内容的相同基本信息。这一行返回相同的属性:

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
Compare-Object -ReferenceObject $aDetail -DifferenceObject $bDetail -Property Name -IncludeEqual -ExcludeDifferent -PassThru |  Sort-Object -Property Origin | Select-Object -Property Name, SideIndicator


Name SideIndicator
---- -------------
ProcessName ==
ParentProcessId ==
PeakPageFileUsage ==
PeakVirtualSize ==
PeakWorkingSetSize ==
Priority ==
PrivatePageCount ==
ProcessId ==
QuotaNonPagedPoolUsage ==
QuotaPagedPoolUsage ==
QuotaPeakNonPagedPoolUsage ==
PageFileUsage ==
QuotaPeakPagedPoolUsage ==
PSComputerName ==
ReadTransferCount ==
SessionId ==
Status ==
TerminationDate ==
ThreadCount ==
UserModeTime ==
VirtualSize ==
WindowsVersion ==
WorkingSetSize ==
ReadOperationCount ==
WriteOperationCount ==
PageFaults ==
OtherOperationCount ==
Handles ==
VM ==
WS ==
Path ==
Caption ==
CreationClassName ==
CreationDate ==
CSCreationClassName ==
CSName ==
Description ==
OtherTransferCount ==
CommandLine ==
ExecutionState ==
Handle ==
HandleCount ==
InstallDate ==
KernelModeTime ==
MaximumWorkingSetSize ==
MinimumWorkingSetSize ==
Name ==
OSCreationClassName ==
OSName ==
评论

PowerShell 技能连载 - 将 Word 文档从 .doc 格式转为 .docx 格式(第 2 部分)

将旧式的 Word 文档转为新的 .docx 格式需要比较多工作量。多谢 PowerShell,您可以自动完成该转换工作:

但是,要做到这一点,有许多额外的步骤。如果要遵守安全指南,您需要了解文档中是否存在宏,并相应地更改扩展名。此外,如果文档处于只读模式,则无法转换该文档,并应跳过转换。如果你批量转换很多文档,有个进度条会很好。

感谢 Wpperal 市的安全专家 Lars Köpcke,增加了只读文档的宏观检查和测试!

以下是一个修订过的函数,能够神奇地批量转换。不过这仍然是一个原型。若果您希望在生产环境下使用它,请确保您理解它并且加入了所有需要的错误处理和报告。如果您不希望该函数重写原有的文件,请增加检测环节。

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
function Convert-WordDocument
{
param
(
# accept path strings or items from Get-ChildItem
[Parameter(Mandatory,ValueFromPipeline,ValueFromPipelineByPropertyName)]
[string]
[Alias('FullName')]
$Path
)

begin
{
# we are collecting all paths first
[Collections.ArrayList]$collector = @()
}

process
{
# find extension
$extension = [System.IO.Path]::GetExtension($Path)

# we only process .doc and .dot files
if ($extension -eq '.doc' -or $extension -eq '.dot')
{
# add to list for later processing
$null = $collector.Add($Path)

}
}
end
{
# pipeline is done, now we can start converting!

Write-Progress -Activity Converting -Status 'Launching Application'

# initialize Word (must be installed)
$word = New-Object -ComObject Word.Application

$counter = 0
Foreach ($Path in $collector)
{
# increment a counter for the progress bar
$counter++

# open document in Word
$doc = $word.Documents.Open($Path)

# determine target document type
# if the doc has macros, use different extensions

[string]$targetExtension = ''
[int]$targetConversion = 0

switch ([System.IO.Path]::GetExtension($Path))
{
'.doc' {
if ($doc.HasVBProject -eq $true)
{
$targetExtension = '.docm'
$targetConversion = 13
}
else
{
$targetExtension = '.docx'
$targetConversion = 16
}
}
'.dot' {
if ($doc.HasVBProject -eq $true)
{
$targetExtension = '.dotm'
$targetConversion = 15
}
else
{
$targetExtension = '.dotx'
$targetConversion = 14
}
}
}

# conversion cannot work for read-only docs
If (!$doc.ActiveWindow.View.ReadingLayout)
{
if ($targetConversion -gt 0)
{
$pathOut = [IO.Path]::ChangeExtension($Path, $targetExtension)

$doc.Convert()
$percent = $counter * 100 / $collector.Count
Write-Progress -Activity 'Converting' -Status $pathOut -PercentComplete $percent
$doc.SaveAs([ref]$PathOut,[ref] $targetConversion)
}
}

$word.ActiveDocument.Close()
}

# quit Word when done
Write-Progress -Activity Converting -Status Done.
$word.Quit()
}
}

以下是调用示例:

1
PS> dir F:\documents -Include *.doc, *.dot -Recurse | Convert-WordDocument
评论

PowerShell 技能连载 - 将 Word 文档从 .doc 格式转为 .docx 格式(第 1 部分)

将旧式的 Word 文档转为新的 .docx 格式需要比较多工作量。多谢 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
33
34
35
36
37
38
39
40
function Convert-WordDocument
{
param
(
[Parameter(Mandatory,ValueFromPipeline,ValueFromPipelineByPropertyName)]
[string]
[Alias('FullName')]
$Path
)
begin
{
# launch Word
$word = New-Object -ComObject Word.Application
}

process
{
# determine target path
$pathOut = [System.IO.Path]::ChangeExtension($Path, '.docx')

# open the document
$doc = $word.Documents.Open($Path)

Write-Progress -Activity 'Converting' -Status $PathOut

# important: do the actual conversion
$doc.Convert()

# save in the appropriate format
$doc.SaveAs([ref]$PathOut,[ref]16)

# close document
$word.ActiveDocument.Close()
}
end
{
# close word
$word.Quit()
}
}
评论

PowerShell 技能连载 - 探索 PowerShell 模块

大多数 PowerShell 的命令都存在于模块中,通过增加新的模块,就可以将新的命令添加到 PowerShell 环境中。要查找一个命令是否位于某个模块中,请使用 Get-Command 命令。下一行代码返回发布 Get-Service 命令的模块:

1
PS C:\> Get-Command -Name Get-Service | Select-Object -ExpandProperty Module

如果 Module 属性是空值,那么改命令不是通过一个模块发布的。对于所有非 PowerShell 命令,例如应用程序,都是这个情况:

1
PS C:\> Get-Command -Name notepad | Select-Object -ExpandProperty Module

下一步,让我们列出您系统中所有可用的模块:

powershell
PS> Get-Module -ListAvailable | Select-Object -Property Name, Path

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

如果更深入地查看这些结果,您会注意到 `Get-Module` 列出多于一个文件夹。PowerShell 指定的缺省模块文件夹是以分号分隔的列表形式存储在 `$env:PSModulePath` 环境变量中,类似应用程序的 `$env:Path`:

PS> $env:PSModulePath
C:\Users\tobia\OneDrive\Dokumente\WindowsPowerShell\Modules;C:\Program Files\WindowsPowerShell\Modules;C:
\Windows\system32\WindowsPowerShell\v1.0\Modules

PS> $env:PSModulePath -split ';'
C:\Users\tobia\OneDrive\Dokumente\WindowsPowerShell\Modules
C:\Program Files\WindowsPowerShell\Modules
C:\Windows\system32\WindowsPowerShell\v1.0\Modules

如果您希望探索一个指定模块的内容,请查看 `Path` 属性:它通常指向一个 .psd1 文件,其中包含模块的元数据。它是指定模块版本和版权信息的地方,并且通常它的 `RootModule` 入口指定了模块的代码。如果这是一个使用 PowerShell 代码构建的模块,那么它是一个 .psm1 文件,否则是一个二进制的 DLL 文件。

要检查模块内容,请在 Windows 资源管理器中打开它的父文件夹。例如,下面一行代码在 Windows 资源管理器中打开 "PrintManagement" 模块(假设它存在于您的机器中):

```powershell
PS> Get-Module -Name PrintManagement -ListAvailable | Select-Object -ExpandProperty Path | Split-Path
C:\Windows\system32\WindowsPowerShell\v1.0\Modules\PrintManagement

PS> explorer (Get-Module -Name PrintManagement -ListAvailable | Select-Object -ExpandProperty Path | Split-Path)

这个快速的演练解释了为什么 PowerShell 没有固定的命令集,以及为什么给定命令在一个系统上可用并且在另一个系统上不可用。新模块可以由操作系统(例如 Windows 10 附带的模块多于 Windows 7)、已安装的软件(例如 SQLServer)、已激活的角色(例如域控制器)引入,并且模块也可以手动安装。

例如这行代码从公开的 PowerShell Gallery 中安装一个免费的模块,并且添加了用于创建各种二维码的新命令:

1
PS C:\> Install-Module -Name QRCodeGenerator -Scope CurrentUser -Force

一旦模块安装完毕,您会得到类似这样的命令,用来创建 Twitter 用户数据文件的二维码:

1
PS C:\> New-QRCodeTwitter -ProfileName tobiaspsp -Show

要查看某个模块中所有命令,请试试这行代码:

1
2
3
4
5
6
7
8
PS C:\> Get-Command -Module QRCodeGenerator

CommandType Name Version Source
----------- ---- ------- ------
Function New-QRCodeGeolocation 1.2 QRCodeGenerator
Function New-QRCodeTwitter 1.2 QRCodeGenerator
Function New-QRCodeVCard 1.2 QRCodeGenerator
Function New-QRCodeWifiAccess 1.2 QRCodeGenerator
评论

PowerShell 技能连载 - WMI Explorer

WMI (Windows Management Instrumentation) is a great information source: you can find almost any information about your computer somewhere. The hard part isn’t the WMI query itself. The hard part is finding out the appropriate WMI class names and properties:

To get information about your BIOS, for example, run this:

PS> Get-CimInstance -ClassName Win32_BIOS


SMBIOSBIOSVersion : 1.0.9
Manufacturer      : Dell Inc.
Name              : 1.0.9
SerialNumber      : 4ZKM0Z2
Version           : DELL   - 20170001

Getting other information is just a question of replacing the class name you are querying, so to get information about your operating system instead, for example, run this:

PS> Get-CimInstance -ClassName Win32_OperatingSystem


SystemDirectory : C:\Windows\system32
Organization    :
BuildNumber     : 18362
RegisteredUser  : tobias.weltner@email.de
SerialNumber    : 00330-50000-00000-AAOEM
Version         : 10.0.18362

To find just about any information and discover the appropriate WMI class names to use, we have created a smart “WMI Explorer” which really is just a simple function. It requires Windows PowerShell (PowerShell 3-5):

function Find-WmiClass
{
  # show all properties, not just 4
  $oldLimit = $FormatEnumerationLimit
  $global:FormatEnumerationLimit = -1
  # list all WMI classes...
  Get-WmiObject -Class * -List |
  # ...with at least 4 properties
  Where-Object { $_.Properties.Count -gt 4 } |
  # let the user select one
  Out-GridView -Title 'Select a class that seems interesting' -OutputMode Single |
  ForEach-Object {
    # query the selected class
    $Name = $_.name
    $props = Get-WmiObject -Class $Name |
    # take the first instance
    Select-Object -Property * -First 1 |
    ForEach-Object {
      # turn the object into a hash table, and exclude empty properties
      $_ | & {
              $_.PSObject.Properties |
        Sort-Object -Property Name |
        Where-Object { $_.Value -ne $null } |
        ForEach-Object {
            $hashtable = [Ordered]@{}} { $hashtable[$_.Name] = $_.Value } { $hashtable }
        } |
        # show the properties and let the user select
        Out-GridView -Title "$name : Select all properties you need (hold CTRL)" -PassThru |
        ForEach-Object {
          # return the selected property names
          $_.Name
        }
    }
    # take all selected properties
    $prop = $props -join ', '
    # create the command for it:
    $a = "Get-CimInstance -Class $Name | Select-Object -Property $prop"
    # place it into the clipboard
    $a | Set-Clipboard
    Write-Warning "Command is also available from the clipboard"
    $a
  }
  # reset format limit
  $global:FormatEnumerationLimit = $oldLimit
}

Here is how it works:

  • Run the code above which adds a new command called Find-WmiClass
  • Run Find-WmiClass to run the function
  • A grid view window opens and displays all WMI classes available in the standard namespace root\cimv2 (if you want to search a different namespace, adjust the code and add the -Namespace parameter to Get-WmiObject calls)
  • Now enter something that you are looking for into the top text field in the grid view window which acts like a filter. If you enter UserName for example, the grid view limits the list to all classes that have “UserName” anywhere in its name or in the name of any of their properties. You now have just a few classes to select from.
  • Select the class you want to investigate, for example “Win32_ComputerSystem”, and click OK in the bottom right corner of the grid.
  • Now the selected class is queried, and the first instance of it displays in another grid view. Each property is displayed on its own line, so you can again filter the display. Hold CTRL to select all properties you want to keep. Then click OK again.
  • The command is created for you and displays in the console. It is also placed into the clipboard, so to test drive it, press CTRL+V, then ENTER.

Thanks to Find-WmiClass, exploring the WMI and finding useful WMI classes and properties has just become very easy.


Twitter This Tip!ReTweet this Tip!

评论

PowerShell 技能连载 - 将对象转换为哈希表

我们经常需要检视一个对象,例如一个进程或是一个 Active Directory 用户。当您在一个网格视图窗口,例如 Out-GridView 中显示一个对象时,整个对象显示为一个长行。

一个好得多的方法是将一个对象转换为哈希表。通过这种方式,每个属性都在独立的一行中显示,这样您就可以用网格视图窗口顶部的文本过滤器搜索每个属性。另外,您可以完全控制转换的过程,这样您可以对对象的属性排序,并且确保它们按字母顺序排序,甚至排除空属性。

以下是一个将对象转换为哈希表并且可以根据需要排除空属性的函数:

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 Convert-ObjectToHashtable
{
param
(
[Parameter(Mandatory,ValueFromPipeline)]
$object,

[Switch]
$ExcludeEmpty
)

process
{
$object.PSObject.Properties |
# sort property names
Sort-Object -Property Name |
# exclude empty properties if requested
Where-Object { $ExcludeEmpty.IsPresent -eq $false -or $_.Value -ne $null } |
ForEach-Object {
$hashtable = [Ordered]@{}} {
$hashtable[$_.Name] = $_.Value
} {
$hashtable
}
}
}

让我们来看看一个对象,例如当前进程,缺省情况下在 Out-GridView 是如何显示的:

1
$process = Get-Process -Id $pid | Out-GridView

和以下这个作对比:

1
$process = Get-Process -Id $pid | Convert-ObjectToHashtable -ExcludeEmpty | Out-GridView

现在分析起来好多了。

评论

PowerShell 技能连载 - 对象的魔法(第 4 部分)

将一个对象转换为一个哈希表如何?通过这种方式,当将对象输出到一个网格视图窗口时,可以每行显示一个对象属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
# get any object
$object = Get-Process -Id $pid

# try and access the PSObject
$hash = $object.PSObject.Properties.Where{$null -ne $_.Value}.Name |
Sort-Object |
ForEach-Object { $hash = [Ordered]@{} } { $hash[$_] = $object.$_ } { $hash }

# output regularly
$object | Out-GridView -Title Regular

# output as a hash table, only non-empty properties, sorted
$hash | Out-GridView -Title Hash
评论

PowerShell 技能连载 - 对象的魔法(第 3 部分)

假设您希望隐藏对象所有没有值(为空)的属性。以下是一个简单的实现:

1
2
3
4
5
6
# get any object
$object = Get-Process -Id $pid

# try and access the PSObject
$propNames = $object.PSObject.Properties.Where{$null -ne $_.Value}.Name
$object | Select-Object -Property $propNames

这段代码将只输出包含值的属性。您甚至可以对属性排序:

1
2
3
4
5
6
# get any object
$object = Get-Process -Id $pid

# try and access the PSObject
$propNames = $object.PSObject.Properties.Where{$null -ne $_.Value}.Name | Sort-Object
$object | Select-Object -Property $propNames
评论

PowerShell 技能连载 - 对象的魔法(第 2 部分)

Via the secret “PSObject” property, you can get detailed information about object members. For example, if you’d like to know which object properties can actually be changed, try this:
通过隐藏的 “PSObject“ 属性,您可以获取对象成员的详细信息。例如,如果您希望知道哪个属性可以被改变,请试试这段代码:

1
2
3
4
5
# get any object
$object = Get-Process -Id $pid

# try and access the PSObject
$object.PSObject.Properties.Where{$_.IsSettable}.Name

结果是进程对象的可以被赋值的属性列表:

MaxWorkingSet
MinWorkingSet
PriorityBoostEnabled
PriorityClass
ProcessorAffinity
StartInfo
SynchronizingObject
EnableRaisingEvents
Site

类似地,您可以轻松地找出所有当前没有值(为空)的属性:

1
2
3
4
5
# get any object
$object = Get-Process -Id $pid

# try and access the PSObject
$object.PSObject.Properties.Where{$null -eq $_.Value}.Name
评论

PowerShell 技能连载 - 对象的魔法(第 1 部分)

在 PowerShell 中,大多数据是以 PSObject 来表示的,它是一个由 PowerShell 添加的特殊的对象“包装器”。要获取这个特殊的包装器,可以通过对象名为 “PSObject“ 的隐藏属性。让我们来看看:

1
2
3
4
5
6
7
8
9
10
11
# get any object
$object = Get-Process -Id $pid

# try and access the PSObject
$object.PSObject

# get another object
$object = "Hello"

# try again
$object.PSObject

如您所见,该 “PSObject“ 基本上是一个对象的描述。并且它包含了许多有用的信息。以下是一部分:

1
2
3
4
5
6
7
8
9
10
# get any object
$object = Get-Process -Id $pid

# try and access the PSObject
$object.PSObject

# find useful information
$object.PSObject.TypeNames | Out-GridView -Title Type
$object.PSObject.Properties | Out-GridView -Title Properties
$object.PSObject.Methods | Out-GridView -Title Methods
评论