有两种方法可以完成这项任务:错误的方法和正确的方法。以下是如何做到这两点。
如果管理员发现自己在做一件事,则可能是确定他们的系统上安装了哪些软件。可能只是为了知道他们安装了什么,或者确定安装的某些软件是否存在漏洞,这些漏洞可以通过安全更新修复或对软件进行审核(可能尚未获准安装)。无论哪种方式,如果您没有像 SCCM 这样的工具或其他第三方工具可用于执行此类审计,则很难找到该软件。
PowerShell 可以通过为我们提供几个不同的选项来执行软件收集,从而帮助我们在本地或远程系统上收集软件。一种是通过 WMI,另一种是通过查看注册表。
WMI 方法
我将首先介绍 WMI,只是因为您永远不应该将它用作收集已安装软件数据的方法。我说的是 WMI 中的Win32_Product类。此类在许多脚本中被滥用,因为虽然它确实为您提供了有关已安装软件的信息,但它带来了与之相关的成本。
为了说明这一点,我将对软件执行 WMI 查找,然后向您展示当我们从 WMI 接收有关此类已安装软件的数据时会发生什么。
Get-WmiObject -Class Win32_Product
该过程缓慢而痛苦,因为在返回更多数据之前,它似乎会挂起不同的时间段。
图 1。
我们确实获得了有用的信息,但它的执行速度以及在后台发生的另一个信息使这种方法成为最没用的方法。我提到当您对此类运行查询时会发生其他事情,该操作是 MSI 安装程序将执行一致性检查并可能修复 Win32_Product 类下的每个已安装项目,然后再将数据返回给您。我们可以通过使用 Get-WinEvent 并查看应用程序日志来查看发生了什么。
Get-WinEvent -FilterHashtable @{Logname='Application';Id=1035} -MaxEvents 10 |
Select-Object -ExpandProperty Message
图 2。
当我开始查询已安装的软件时,这只是淹没了我的计算机的 400 多个事件中的 10 个。如果您决定在所有系统上收集数据,那么在性能方面并在整个域中发生并不是一件好事。帮自己一个忙,不惜一切代价避免与 Win32_Product 打交道!
注册表方法
另一种方法是挖掘注册表以获取有关已安装软件的信息。确定注册表的范围,我们可以找到两条路径,其中包含软件所需的所有数据。这些路径是:
- HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall
- HKLM:\SOFTWARE\Wow6432node\Microsoft\Windows\CurrentVersion\Uninstall
使用 Regedit 快速浏览这些路径之一,我们发现我们绝对走在正确的道路上。
[点击图片查看大图。] 图 3。
卸载程序正下方有很多 GUID,但在每个 GUID 中,我们可以看到有关我们可以在数据收集中使用的软件的更多信息。
我将预先展示所有代码,然后逐步介绍一些我认为很重要的项目,这些项目有助于我们收集信息。
Function Get-Software {
[OutputType('System.Software.Inventory')]
[Cmdletbinding()]
Param(
[Parameter(ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$True)]
[String[]]$Computername=$env:COMPUTERNAME
)
Begin {
}
Process {
ForEach ($Computer in $Computername){
If (Test-Connection -ComputerName $Computer -Count 1 -Quiet) {
$Paths = @("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall","SOFTWARE\\Wow6432node\\Microsoft\\Windows\\CurrentVersion\\Uninstall")
ForEach($Path in $Paths) {
Write-Verbose "Checking Path: $Path"
# Create an instance of the Registry Object and open the HKLM base key
Try {
$reg=[microsoft.win32.registrykey]::OpenRemoteBaseKey('LocalMachine',$Computer,'Registry64')
} Catch {
Write-Error $_
Continue
}
# Drill down into the Uninstall key using the OpenSubKey Method
Try {
$regkey=$reg.OpenSubKey($Path)
# Retrieve an array of string that contain all the subkey names
$subkeys=$regkey.GetSubKeyNames()
# Open each Subkey and use GetValue Method to return the required values for each
ForEach ($key in $subkeys){
Write-Verbose "Key: $Key"
$thisKey=$Path+"\\"+$key
Try {
$thisSubKey=$reg.OpenSubKey($thisKey)
# Prevent Objects with empty DisplayName
$DisplayName = $thisSubKey.getValue("DisplayName")
If ($DisplayName -AND $DisplayName -notmatch '^Update for|rollup|^Security Update|^Service Pack|^HotFix') {
$Date = $thisSubKey.GetValue('InstallDate')
If ($Date) {
Try {
$Date = [datetime]::ParseExact($Date, 'yyyyMMdd', $Null)
} Catch{
Write-Warning "$($Computer): $_ <$($Date)>"
$Date = $Null
}
}
# Create New Object with empty Properties
$Publisher = Try {
$thisSubKey.GetValue('Publisher').Trim()
}
Catch {
$thisSubKey.GetValue('Publisher')
}
$Version = Try {
#Some weirdness with trailing [char]0 on some strings
$thisSubKey.GetValue('DisplayVersion').TrimEnd(([char[]](32,0)))
}
Catch {
$thisSubKey.GetValue('DisplayVersion')
}
$UninstallString = Try {
$thisSubKey.GetValue('UninstallString').Trim()
}
Catch {
$thisSubKey.GetValue('UninstallString')
}
$InstallLocation = Try {
$thisSubKey.GetValue('InstallLocation').Trim()
}
Catch {
$thisSubKey.GetValue('InstallLocation')
}
$InstallSource = Try {
$thisSubKey.GetValue('InstallSource').Trim()
}
Catch {
$thisSubKey.GetValue('InstallSource')
}
$HelpLink = Try {
$thisSubKey.GetValue('HelpLink').Trim()
}
Catch {
$thisSubKey.GetValue('HelpLink')
}
$Object = [pscustomobject]@{
Computername = $Computer
DisplayName = $DisplayName
Version = $Version
InstallDate = $Date
Publisher = $Publisher
UninstallString = $UninstallString
InstallLocation = $InstallLocation
InstallSource = $InstallSource
HelpLink = $thisSubKey.GetValue('HelpLink')
EstimatedSizeMB = [decimal]([math]::Round(($thisSubKey.GetValue('EstimatedSize')*1024)/1MB,2))
}
$Object.pstypenames.insert(0,'System.Software.Inventory')
Write-Output $Object
}
} Catch {
Write-Warning "$Key : $_"
}
}
} Catch {}
$reg.Close()
}
} Else {
Write-Error "$($Computer): unable to reach remote system!"
}
}
}
}
需要强调的一件事是我使用$reg=[microsoft.win32.registrykey]::OpenRemoteBaseKey('LocalMachine',$Computer,'Registry64')它不仅可以连接到我的本地计算机,还可以连接到通过注册表工作的远程系统。目前,查看远程注册表的唯一其他方法是使用 PowerShell 远程处理,它可能在您的环境中的所有系统中都可用,也可能不可用。
建立注册表连接后,我只需开始处理卸载程序下的每个 GUID 并开始收集我认为有用的信息。为此,我使用OpenSubkey()获取每个 GUID,然后通过调用GetValue()并提供我想要的每个属性项的正确名称来处理所有数据点。
最后,我使用每个变量中收集的所有数据并使用[pscustomobject]创建一个新对象并将该对象输出到控制台,以便我可以对该数据进行排序、分组或将其发送到 CSV 之类的东西以供以后查看。
Get-Software 函数演示
函数完成后,我现在可以通过按名称调用它来使用它,这将允许它查询正在运行代码的本地机器,或者使用 Computername 参数。
Get-Software
图 4。
这只是我安装的一小部分软件,但正如您所见,我的客户端上安装的每个软件都有相当多的有用信息。
性能测试
为了强调使用我编写的 Get-Software 函数(使用注册表作为收集软件的手段)和使用 Win32_Product 类之间的性能影响,这将导致比其价值更多的烦恼,我使用 Measure 进行了简单的性能检查-命令:
#Using Win32_Product
$Win32Product = Measure-Command {Get-WmiObject -Class Win32_product}
#Using Get-Software (Registry)
$GetSoftware = Measure-Command {Get-Software}
[pscustomobject]@{
Win32Product = $Win32Product.TotalMilliseconds
GetSoftware = $GetSoftware.TotalMilliseconds
}
图 5。
结果不言自明:WMI 为 24 秒,而注册表则不到一秒。所以请记住,如果您要在您的系统上查找软件,请确保您使用我的 Get-Software 功能或制作您自己的使用注册表来执行查找的功能。