Loop through servers and output results along with errors - powershell

I wrote a simple PowerShell script to retrieve a list of servers' last boot time and output the results to grid view. The results are immediately shown in the grid window but and comes to a short pause whenever a server is not responding to the get command, either due to WMI not running or class not registered. It then displays the error in PS and move to the next server.
Now, the results aren't helpful unless the "not responding" servers are shown in the results windows.
$servers = ('serverx','serverb')
Get-WmiObject -Class Win32_OperatingSystem -ComputerName $servers |
select csname, #{LABEL='LastBootUpTime';EXPRESSION={$_.ConvertToDateTime($_.LastBootupTime)}},
#{LABEL='LocalTime';EXPRESSION={$_.ConvertToDateTime($_.LocalDateTime)}},
#{LABEL='UpTime';EXPRESSION={(Get-Date) - $_.ConvertToDateTime($_.LastBootupTime)}},
#{LABEL='OS';EXPRESSION={$_.Caption}} |
Out-GridView
Errors type shown in PS window in Red:
Get-WmiObject : Class not registered (Exception from HRESULT: 0x80040154 (REGDB_E_CLASSNOTREG)) At line:1 char:12
Get-WmiObject : The RPC server is unavailable. (Exception from HRESULT: 0x800706BA) At line:1 char:12
Edit: How do I can i output the good results along with the server name if the servers that responded with an error?

For your desired result you need to query the servers individually and construct a custom object if the query fails:
$svr = 'serverx'
try {
Get-WmiObject Win32_OperatingSystem -Computer $svr -EA Stop |
select csname, #{n='LocalTime';e={...}},
#{n='UpTime';e={...}}, #{n='OS';e={...}}
} catch {
New-Object -Type PSObject -Property #{
csname = $svr
LocalTime = $null
UpTime = $null
OS = $null
}
}
Run this in a loop
$servers | ForEach-Object {
...
} | Out-GridView
Use background jobs (or something similar) instead of a plain loop to speed up the checks by running them in parallel rather than sequentially. Spawn each check as a job in the background and check for completed jobs in a loop until all jobs have completed. Collect the output from completed jobs.

Here is the full script that loops through the servers, catches non-terminating error and output to a window.
$svr = ('localhost','fail')
$Output = Foreach ($server in $svr)
{
try {
Get-WmiObject Win32_OperatingSystem -ComputerName $server -EA STOP |
select csname, #{n='LocalTime';e={$_.ConverttoDateTime($_.lastbootuptime)}},
#{n='UpTime';e={....}}, #{n='OS';e={"...."}}
} catch {
New-Object -Type PSObject -Property #{
Csname = $server
LocalTime = $null
UpTime = $null
OS = "Error" #$null
}
}
}
$output | Out-GridView

Related

PowerShell Invoke-Command timeout if no answer from frozen AD-Server

I'm writing a PowerShell-Script that reads all shares from all AD-Servers and outputs them into a csv-file. At the same time the script is saving all occuring errors and outputs them into an error-log. The script will be run as a weekly task. When I run the script, all goes well until it gets to a server that has frozen. In that case, the script will just run forever because it gets no answer from the server.
Now I need to add some sort of timeout that skips a server after it doesn't recieve an answer for a specific amount of time. How would I do that with my existing code?
My Code:
$computers = (Get-Content C:\PowerShell\Shares\serverlist.txt).ForEach({
if(-not [string]::IsNullOrWhiteSpace($_))
{
"$_.domain.com"
}
})
$remoteCode = {
Get-SmbShare | Where-Object Path | Get-Acl |
Select-Object -Property "PSChildName", "Path", "Group", "AccessToString"
}
$results = Invoke-Command -ComputerName $computers -ScriptBlock $remoteCode 2>&1
$errors, $good = $results.Where({$_ -is [System.Management.Automation.ErrorRecord]}, 'Split')
$good | Sort-Object PSComputerName | Select-Object "PSComputerName", "PSChildName", "Path", "Group", #{ Name = "AccessToString"; Expression = { $_.AccessToString -replace("268435456", "FullControl") -replace("-1610612736", "ReadAndExecute")}} | export-csv -path C:\PowerShell\Shares\shares.csv -NoTypeInformation -delimiter ";"
$errors.Exception.Message | Set-Content $error_logfile -Encoding Unicode
NOTE: This answer is pretty much useless, in an ideal world, -OperationTimeout would do what it's name implies, however, as the helpful comment from mklement0 states:
Unfortunately, the OperationTimeout session option doesn't do what its name suggests: see GitHub issue #15696. Implementing an actual operation-duration timeout is the subject of GitHub proposal #5434, which suggest adding a -Timeout parameter to Invoke-Command.
If you feel this would be a great implementation for future versions of PowerShell, consider up-voting his proposal!
You could use PSSessionOption with a Operation Timeout and Open Timeout below the default values (3 minutes):
See the Parameter section of New-PSSessionOption documentation:
-OpenTimeout
Determines how long the client computer waits for the session connection to be established. When the interval expires, the command to establish the connection fails.
-OperationTimeout
Determines the maximum time WinRM waits for positive connection tests from a live connection before initiating a connection time-out.
$timeOut = 30000 # => 30 seconds
$psso = New-PSSessionOption -OpenTimeout $timeOut -OperationTimeout $timeOut
$session = (Get-Content C:\PowerShell\Shares\serverlist.txt).ForEach({
if(-not [string]::IsNullOrWhiteSpace($_)) {
try {
New-PSSession -ComputerName "$_.domain.com" -SessionOption $psso
}
catch {
Write-Warning $_.Exception.Message
}
}
})
Then the rest of the script is the same, except for the use of -Session instead -ComputerName for Invoke-Command:
$results = Invoke-Command -Session $session -ScriptBlock $remoteCode 2>&1
And lastly, after you're done with the remote connections, you would need to remove the PSSessions:
Remove-PSSession $session

Using Invoke-Command with Variables in ScriptBlock

New to PowerShell and learning through writing random scripts using the help info. I've tried the following 3 ways to properly get variables into the ScriptBlock(along with way too many small variations to list) with listed error message wrapped in **:
do
{
try {
[ValidateRange(1,7)][int]$days = Read-Host "Let's pull up some Warning event logs. How many days back would you like to go back? (1-7)"
} catch {}
} until ($?)
do
{
try {
[ValidateSet('desktop','documents',IgnoreCase)]$location = Read-Host "Would you like me to save the log on your Desktop or in your Documents?"
} catch {}
} until ($?)
$filename = Read-Host "What would you like to name the file?"
$DaysAgo = [datetime]::Now.AddDays(-$days)
Invoke-Command -AsJob -Jobname JobEventLog -ScriptBlock {Get-EventLog -logname system | Where-Object EntryType -eq Warning | where TimeGenerated -ge $DaysAgo | Out-File $HOME\$location\$filename.txt}
Invoke-Command : Parameter set cannot be resolved using the specified named parameters.
do
{
try {
[ValidateRange(1,7)][int]$days = Read-Host "Let's pull up some Warning event logs. How many days back would you like to go back? (1-7)"
} catch {}
} until ($?)
do
{
try {
[ValidateSet('desktop','documents',IgnoreCase)]$location = Read-Host "Would you like me to save the log on your Desktop or in your Documents?"
} catch {}
} until ($?)
$filename = Read-Host "What would you like to name the file?"
$DaysAgo = [datetime]::Now.AddDays(-$days)
Invoke-Command -AsJob -Jobname JobEventLog -ScriptBlock {Get-EventLog -logname system | Where-Object EntryType -eq Warning | where TimeGenerated -ge $Using:$DaysAgo | Out-File $Using:HOME\$Using:location\$Using:filename.txt}
Invoke-Command : Parameter set cannot be resolved using the specified named parameters.
do
{
try {
[ValidateRange(1,7)][int]$days = Read-Host "Let's pull up some Warning event logs. How many days back would you like to go back? (1-7)"
} catch {}
} until ($?)
do
{
try {
[ValidateSet('desktop','documents',IgnoreCase)]$location = Read-Host "Would you like me to save the log on your Desktop or in your Documents?"
} catch {}
} until ($?)
$filename = Read-Host "What would you like to name the file?"
Write-Host "Processing..."
$DaysAgo = [datetime]::Now.AddDays(-$days)
$parameters = #{
ScriptBlock = { Param ($Arg1,$Arg2,$Arg3) Invoke-Command -AsJob -Jobname JobEventLog -ScriptBlock {Get-EventLog -logname system | Where-Object source -eq DCOM | where TimeGenerated -ge $Arg1 | Out-File "$HOME\$Arg2\$Arg3.txt"}}
JobName = "DCOM"
ArgumentList = ($DaysAgo,$location,$filename)
}
Invoke-Command #parameters
Invoke-Command : Cannot validate argument on parameter 'ScriptBlock'. The argument is null. Provide a valid value for the argument, and then try running the command again.
I'm just looking to have user input how far back they want to view Event Logs, where to save it, and what to name it. I've been able to work my way through everything so far until I hit the Invoke-Command line and haven't been able to get through it. I prefer the one line style of 1 and 2 over the parameters style, however after spending way too much time using the help_Invoke-Command-full and googling I'm throwing in the towel over what I'm sure is a simple error on my syntax.
You can use $args inside the scriptblock, see an example:
$DaysAgo = [datetime]::Now.AddDays(-$days)
Invoke-Command -AsJob -Jobname JobEventLog -ScriptBlock {
Get-EventLog -logname system | Where-Object EntryType -eq Warning |
where TimeGenerated -ge $args[0] |
Out-File $HOME\$location\$filename.txt
} -ArgumentList $DaysAgo
Add the arguments at the end of the Invoke-Command like in the example and use $args[0] for the first argument, $args[1] for the second and so on...
This works for me. The computer is localhost as a test, at an elevated prompt, which you would need for the system log anyway. Level 3 is warning. If it was for the same computer you wouldn't need invoke-command at all.
$location = 'foo'
$filename = 'myfile'
$date = get-date
$daysago = $date.adddays(-1)
invoke-command localhost { param($daysago, $location, $filename)
get-winevent #{logname = 'system'; level = 3; starttime = $daysago} |
out-file $home\$location\$filename.txt } -args $daysago,$location,$filename
In order to use Invoke-Command's -AsJob switch, you must execute code remotely, such as by targeting a different computer with the -ComputerName or -Session arguments.
In the absence of such arguments, your command would run locally, but it fails due to the syntactic restriction described above.
If you want to run a job locally, use Start-Job directly:
$job = Start-Job -Name JobEventLog -ScriptBlock {
Get-EventLog -logname system |
Where-Object EntryType -eq Warning |
Where-Object TimeGenerated -ge $using:DaysAgo |
Out-File $HOME\$using:location\$using:filename.txt
}
Note: Since your background script block references variables from the caller's scope, they must be referenced via the $using: scope (as you've also done in your last Invoke-Command-based attempt). This requirement also applies to script blocks executed remotely, such as via Invoke-Command -ComputerName - see this answer for background information. The alternative is to pass arguments to the script block, via the -ArgumentList (-Args) parameter, though the $using: approach is usually simpler.
Start-Job returns a job-information object (System.Management.Automation.Job), which you can use to monitor the progress of and obtain output from the background job, using the various *-Job cmdlets, notably Wait-Job and Receive-Job - see the about_Jobs conceptual help topic.
Generally, using Invoke-Command for local code execution, while technically supported, is rarely necessary.
For direct, synchronous invocation (not as a job) of a command or script block, use &, the call operator (not needed for single commands, as long as the command name isn't quoted or specified via a variable), or, for execution directly in the caller's scope, ., the dot-sourcing operator (. { ... }).
You have a couple of options. Since you're only running this on your local machine, you can use Start-Job instead of Invoke-Command.
That being said, the problem that you're running into is 2-fold. First, if you're running the Invoke-Command cmdlet, you'll need to specify the ComputerName parameter. Even though it's an optional parameter, you'll need to use it to tell Powershell which parameter set you're using, otherwise it's going to get confused.
Secondly, you'll need to pass the arguments into the scriptblock. This is because Start-Job and Invoke-Command are part of PSRemoting and will actually look for environment variables on the specified computer instead of variables that you've declared in your script.
Here's what worked for me:
Invoke-Command -ComputerName $(hostname) -AsJob -JobName "TestJob" -ScriptBlock { Get-EventLog -logname system | Where-Object EntryType -eq Warning | Where-Object -property TimeGenerated -ge $args[0] | Out-File "$HOME\$($args[1])\$($args[2]).txt" } -ArgumentList $DaysAgo, $location, $filename
The Invoke-Command option is powerful if you're wanting to get this information from other devices on your network.
And here's the Start-Job version:
Start-Job -Name "TestJob2" -ScriptBlock { Get-EventLog -logname system | Where-Object EntryType -eq Warning | Where-Object -property TimeGenerated -ge $args[0] | Out-File "$HOME\$($args[1])\$($args[2]).txt" } -ArgumentList $DaysAgo, $location, $filename

Kill a process if it reaches x% of CPU usage

I want to stop processes that are running higher than 14% CPU usage.
$process = get-process
foreach ($proc in (Get-WmiObject Win32_Processor)){
if($proc.numberofcores -eq $null){
$cores++
}else{
$cores = $cores + $proc.numberofcores
}
}
foreach($name in $process){
$processName = $name.processName
foreach($hog in $processName){
$cpuusage = [Math]::round(((((Get-Counter "\Process($processName)\%
Processor Time" -MaxSamples 2).Countersamples)[0].CookedValue)/$cores),2)
if($cpuusage -gt 14){
Stop-Process -Name $processName
}
}
}
I am getting the following as an error, an nothing else. I expect the Idle(0) not to work, but nothing else is being killed.
Stop-Process : Cannot stop process "Idle (0)" because of the following
error: Access is denied
At line:14 char:17
+ Stop-Process -Name $processName
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : CloseError: (System.Diagnostics.Process
(Idle):Process) [Stop-Process], ProcessCommandException
+ FullyQualifiedErrorId :
CouldNotStopProcess,Microsoft.PowerShell.Commands.StopProcessCommand
I have tried to replace the $processName variables within the second foreach loop to $hog and I still get the same error.
After reading #JosefZ answer I got something that satisfies what I require for my class. Posting it here for reference;
$process = get-process
foreach ($pro in $process){
$name = $pro.ProcessName
$CpuCores = (Get-WmiObject -Class Win32_Processor).NumberOfCores
$CpuValue = ((Get-Counter "\Process($name)\% Processor Time").CounterSamples.CookedValue)/$CpuCores
$percent = [Decimal]::Round($CpuValue, 3)
if($percent -ge 15){
Stop-Process -Name $name
$wshell = New-Object -ComObject Wscript.Shell
$wshell.Popup("Process $name was using more than $percent % CPU. We have eliminated it.",0,"Ok",0x1)
}
}
Note that Performance counters are often protected by access control lists (ACLs). To get all available performance counters, open Windows PowerShell with the "Run as administrator" option and Your ability to stop processes depends on your permissions.
The following script was used for debugging with CPU-time consuming wmic path cim_datafile and antivirus full scan:
Set-StrictMode -Version latest
$process = get-process
$cores = 0
foreach ($proc in (Get-WmiObject Win32_Processor)){
if($proc.numberofcores -eq $null){
$cores++
}else{
$cores = $cores + $proc.numberofcores
}
}
### $cores
foreach($name in $process){
$processName = $name.processName
if ( $processName -notmatch "^Idle" ) {
foreach($hog in $processName){
$cpuusage = -1
$cpuusage = [Math]::round(((((Get-Counter "\Process($processName)\% Processor Time" -MaxSamples 2).Countersamples)[0].CookedValue)/$cores),2)
if ($cpuusage -gt 14) {
Stop-Process -Name $processName -PassThru -ErrorAction Continue
### "{0} {1} {2} {3}" -f '+', $cpuusage, $name.Id, $processName
} else {
if($cpuusage -ne 0){
### "{0} {1} {2} {3}" -f '-', $cpuusage, $name.Id, $processName
}
}
}
}
}
Dirty solution: Stop-Process -Name $processName -ErrorAction SilentlyContinue -PassThru
"Idle" means "inactive" (not operating or being used). When the "System Idle Process" is at 100 %, that means nothing is using your CPU resources.
Read Stopping Processes (Stop-Process) at MSDN:
Windows PowerShell gives you flexibility for listing processes, but
what about stopping a process?
The Stop-Process cmdlet takes a Name or Id to specify a process
you want to stop. Your ability to stop processes depends on your
permissions. Some processes cannot be stopped. For example, if you
try to stop the idle process, you get an error:
PS> Stop-Process -Name Idle
Stop-Process : Process 'Idle (0)' cannot be stopped due to the following error:
Access is denied
At line:1 char:13
+ Stop-Process <<<< -Name Idle
Read Get-Help 'Stop-Process' -Online
Outputs
None, System.Diagnostics.Process
This cmdlet returns a System.Diagnostics.Process object that represents the stopped process, if you specify the PassThru
parameter. Otherwise, this cmdlet does not generate any output.
Couple of additional pointers for you. Firstly you should use Win32_ComputerSystem and NumberOfLogicalProcessors instead of Win32_Processor with NumberOfCores. The reason being, that the performance counters account for HyperThreading on systems that have it, so your calculation based off the physical cores will give you a processor time value twice the size of what it should be. For example, on my machine with 6 Physical Cores, I have 12 logical processors due to HyperThreading. Using your original calculation, a process using 8% CPU would be reported as 16% and thus be incorrectly stopped. The Win32_ComputerSystem version will return all logical processors across all physical CPU's, so multi socketed servers would also be calculated correctly.
C:\WINDOWS\system32> (Get-WmiObject win32_processor).NumberofCores
6
C:\WINDOWS\system32> (Get-WmiObject win32_computersystem).Numberoflogicalprocessors
12
Second, you should be stopping the process by it's ID not by Name, as this will have unintended consequences. A simple example is Chrome, which has a process per tab, all of which have the Name Chrome. So a single tab could be having issues causing high CPU, but your script calling Stop-Process -Name Chrome would close all instances of Chrome, including the ones that aren't doing anything.
The following example script resolves both of these issues:
#Get all cores, which includes virtual cores from hyperthreading
$cores = (Get-WmiObject Win32_ComputerSystem).NumberOfLogicalProcessors
#Get all process with there ID's, excluding processes you can't stop.
$processes = ((Get-Counter "\Process(*)\ID Process").CounterSamples).where({$_.InstanceName -notin "idle","_total","system"})
#Get cpu time for all processes
$cputime = $processes.Path.Replace("id process", "% Processor Time") | get-counter | select -ExpandProperty CounterSamples
#Get the processes with above 14% utilisation.
$highUsage = $cputime.where({[Math]::round($_.CookedValue / $cores,2) -gt 14})
# For each high usage process, grab it's process ID from the processes list, by matching on the relevant part of the path
$highUsage |%{
$path = $_.Path
$id = $processes.where({$_.Path -like "*$($path.Split('(')[1].Split(')')[0])*"}) | select -ExpandProperty CookedValue
Stop-Process -Id $id -Force -ErrorAction SilentlyContinue
}
Note that the usage of the .where( syntax requires PowerShell 5 or above. This script also has the added bonus of being much faster than calling Get-Counter in a foreach loop.

PowerShell Invoke-Command Returns Blank Data?

Been trying to solve this for a bit and can't seem to figure it out.
I have the following script:
$Servers = Get-Content -Path "C:\Utilities_PowerShell\ServerList.txt"
$IISServiceName1 = 'W3SVC'
$IISServiceName2 = 'IISAdmin'
$IISServiceName3 = 'WAS'
$IISarrService = Get-Service -Name $IISServiceName1,$IISServiceName2,$IISServiceName3
$IISarrServiceCheck = Get-Service -Name $IISServiceName1,$IISServiceName2,$IISServiceName3 -ErrorAction SilentlyContinue -ErrorVariable NoService
function IISServiceStatus # Checks for status of IIS services
{
param (
$IISServiceName1,
$IISServiceName2,
$IISServiceName3,
$IISarrService,
$IISarrServiceCheck
)
if (Get-Service -Name $IISServiceName1,$IISServiceName2,$IISServiceName3)
{
Write-Host "Status of IIS service(s) on $env:ComputerName :"
Get-Service -Name $IISServiceName1,$IISServiceName2,$IISServiceName3 | Select Name,DisplayName,Status | Format-Table -AutoSize
}
else
{
Write-Host " No IIS service(s) were found..." -foreground "red"
}
}
$Sessions = New-PSSession -ComputerName $Servers
$EndJobs = $Sessions | ForEach-Object {
Invoke-Command -Session $_ -ScriptBlock ${function:IISServiceStatus} -AsJob -ArgumentList $IISServiceName1, $IISServiceName2, $IISServiceName3, $IISarrService, $IISarrServiceCheck | Wait-Job | Receive-Job
Write-Host " "
}
Whenever I run it, all I get is the output of:
Status of IIS service(s) on *PC* :
If I run the function outside of a loop/invoke-command, the results are absolutely perfect. What is wrong with my remote loop?
I've tried putting the variables inside the function, I've tried running invoke-command without the argument list, etc.
Update: 3/17/16
Turns out...if I run my actual script as is, the result of $EndJobs is weird in that it outputs ALL services in one table and then the three IIS services in another table. This would explain why when I run my invoke-command (stopIIS) scriptblock...I had to reboot the whole server because it took all of the services down.
These functions run PERFECTLY when not run via remote/invoke-command.
What the heck...invoke-command is seriously screwing with my stuff!
Anyone have any ideas/tips on how I can run my local script (which works 100%) on a set of servers from a text file without weird issues like this? Is invoke-command the only way?
do you have the same problem if you wrap it all into the script block like this?
$Servers = Get-Content 'C:\Utilities_PowerShell\ServerList.txt'
$Sessions = New-PSSession -ComputerName $Servers
$EndJobs = $Sessions | ForEach-Object {
Invoke-Command -Session $_ -ScriptBlock {
$IISServiceName1 = 'W3SVC'
$IISServiceName2 = 'IISAdmin'
$IISServiceName3 = 'WAS'
$IISarrService = Get-Service -Name $IISServiceName1,$IISServiceName2,$IISServiceName3
$IISarrServiceCheck = Get-Service -Name $IISServiceName1,$IISServiceName2,$IISServiceName3 -ErrorAction SilentlyContinue -ErrorVariable NoService
function IISServiceStatus { # Checks for status of IIS services
param (
$IISServiceName1,
$IISServiceName2,
$IISServiceName3,
$IISarrService,
$IISarrServiceCheck
)
if (Get-Service -Name $IISServiceName1,$IISServiceName2,$IISServiceName3) {
Write-Host "Status of IIS service(s) on $env:ComputerName :"
Get-Service -Name $IISServiceName1,$IISServiceName2,$IISServiceName3 | Select Name,DisplayName,Status | Format-Table -AutoSize
} else {
Write-Host ' No IIS service(s) were found...' -ForegroundColor Red
}
}
IISServiceStatus $IISServiceName1 $IISServiceName2 $IISServiceName3 $IISarrService $IISarrServiceCheck
} -AsJob | Wait-Job | Receive-Job
Write-Host ' '
}
$EndJobs
I'm having a similar issue. I'm using credssp to test 2nd hop auth for an automation for shutting down a production environment cleanly. My script has 3 sections; session setup, the invoke, session teardown. If I run each piece separately, I get output. If I run the whole script, I get blank lines matching the amount of output I get when I run them separately... there's nothing fancy in my invoke (backtick line continuation - I prefer Python's formatting paradigm better than Powershell/C#):
Invoke-Command `
-Session $workingSession `
-ScriptBlock {
get-service *spool* -ComputerName server01
}

Powershell : How to get Antivirus product details

We have over 1500 servers. Windows 2003, 2008 and 2012. I have to gather the details of antivirus(Product Name & Version) on these servers.
There could be multiple antivirus product.
I am not sure powershell script will work on 2003 server.
So, far i tried below scripts but not got useful information.
$av = get-wmiobject -class "Win32_Product" -namespace "root\cimv2" `
-computername "." -filter "Name like '%antivirus%'"
Below script is working fine on client operating system.
$wmiQuery = "SELECT * FROM AntiVirusProduct"
$AntivirusProduct = Get-WmiObject -Namespace "root\SecurityCenter2" -Query $wmiQuery #psboundparameters # -ErrorVariable myError -ErrorAction 'SilentlyContinue'
Write-host $AntivirusProduct.displayName
Can anybody advise me on this?
I am trying to get the details of antivirus(Product & Version)
What do i need to do for win server 2003?
You were on the right path, the following Powershell script works.
function Get-AntiVirusProduct {
[CmdletBinding()]
param (
[parameter(ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)]
[Alias('name')]
$computername=$env:computername
)
#$AntivirusProducts = Get-WmiObject -Namespace "root\SecurityCenter2" -Query $wmiQuery #psboundparameters # -ErrorVariable myError -ErrorAction 'SilentlyContinue' # did not work
$AntiVirusProducts = Get-WmiObject -Namespace "root\SecurityCenter2" -Class AntiVirusProduct -ComputerName $computername
$ret = #()
foreach($AntiVirusProduct in $AntiVirusProducts){
#Switch to determine the status of antivirus definitions and real-time protection.
#The values in this switch-statement are retrieved from the following website: http://community.kaseya.com/resources/m/knowexch/1020.aspx
switch ($AntiVirusProduct.productState) {
"262144" {$defstatus = "Up to date" ;$rtstatus = "Disabled"}
"262160" {$defstatus = "Out of date" ;$rtstatus = "Disabled"}
"266240" {$defstatus = "Up to date" ;$rtstatus = "Enabled"}
"266256" {$defstatus = "Out of date" ;$rtstatus = "Enabled"}
"393216" {$defstatus = "Up to date" ;$rtstatus = "Disabled"}
"393232" {$defstatus = "Out of date" ;$rtstatus = "Disabled"}
"393488" {$defstatus = "Out of date" ;$rtstatus = "Disabled"}
"397312" {$defstatus = "Up to date" ;$rtstatus = "Enabled"}
"397328" {$defstatus = "Out of date" ;$rtstatus = "Enabled"}
"397584" {$defstatus = "Out of date" ;$rtstatus = "Enabled"}
default {$defstatus = "Unknown" ;$rtstatus = "Unknown"}
}
#Create hash-table for each computer
$ht = #{}
$ht.Computername = $computername
$ht.Name = $AntiVirusProduct.displayName
$ht.'Product GUID' = $AntiVirusProduct.instanceGuid
$ht.'Product Executable' = $AntiVirusProduct.pathToSignedProductExe
$ht.'Reporting Exe' = $AntiVirusProduct.pathToSignedReportingExe
$ht.'Definition Status' = $defstatus
$ht.'Real-time Protection Status' = $rtstatus
#Create a new object for each computer
$ret += New-Object -TypeName PSObject -Property $ht
}
Return $ret
}
Get-AntiVirusProduct
Output:
Product GUID : {B0D0C4F4-7F0B-0434-B825-1213C45DAE01}
Name : CylancePROTECT
Real-time Protection Status : Enabled
Computername : HOSTNAME
Product Executable : C:\Program Files\Cylance\Desktop\CylanceSvc.exe
Reporting Exe : C:\Program Files\Cylance\Desktop\CylanceSvc.exe
Definition Status : Up to date
Product GUID : {D68DDC3A-831F-4fae-9E44-DA132C1ACF46}
Name : Windows Defender
Real-time Protection Status : Unknown
Computername : HOSTNAME
Product Executable : windowsdefender://
Reporting Exe : %ProgramFiles%\Windows Defender\MsMpeng.exe
Definition Status : Unknown
Instead of relying on running processes, you could query the registry :
$computerList = "localhost", "localhost"
$filter = "antivirus"
$results = #()
foreach($computerName in $computerList) {
$hive = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine, $computerName)
$regPathList = "SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall",
"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"
foreach($regPath in $regPathList) {
if($key = $hive.OpenSubKey($regPath)) {
if($subkeyNames = $key.GetSubKeyNames()) {
foreach($subkeyName in $subkeyNames) {
$productKey = $key.OpenSubKey($subkeyName)
$productName = $productKey.GetValue("DisplayName")
$productVersion = $productKey.GetValue("DisplayVersion")
$productComments = $productKey.GetValue("Comments")
if(($productName -match $filter) -or ($productComments -match $filter)) {
$resultObj = [PSCustomObject]#{
Host = $computerName
Product = $productName
Version = $productVersion
Comments = $productComments
}
$results += $resultObj
}
}
}
}
$key.Close()
}
}
$results | ft -au
Example output :
Host Product Version Comments
---- ------- ------- --------
localhost Avast Free Antivirus 10.4.2233
localhost Avast Free Antivirus 10.4.2233
Would this work for you? It's written in PowerShell v2, so if you have that installed on your 2003 servers, it will run on all the servers. This code will give you a CSV of this data from whichever machines run the script that have a service with the description including the word "virus" (which I thought better than "antivirus" because some services use "anti-virus" instead). If they all have access to a shared resource, you can prepend that shared resource directory to the $Filename variable and it will name each report starting with that computer's name and dump your reports there.
invoke-command -computername Server01, Server02 -filepath c:\Scripts\get_av_info.ps1
Assuming the script is saved as c:\Scripts\get_av_info.ps1, that should run it on whatever machines you specify, or if you have a CSV of all the machines you want to run the script, ForEach it. I didn't try this, so I can't verify the remote invoking.
$Date = (Get-Date).ToString('yyyy-MM-dd')
$localhost = $env:computername
$Filename = "C:\" + $localhost + "_" + $Date + "_AV_FileInfo.csv"
$AV = get-process | ?{$_.Description -like "*virus*"}
$Process = ForEach($a in $AV){
$ID = $($a.Id)
get-process -Id $ID -FileVersionInfo
}
$Process | select "CompanyName","FileBuildPart","FileDescription","FileName","FileVersion","ProductName","ProductPrivatePart","ProductVersion","SpecialBuild" | Export-Csv $Filename -NoTypeInformation
There are a LOT of options, I just picked ones I thought you'd want. You could probably also combine the reports to one by adding a shared resource to the Filename and having it -Append, but you would run the risk of multiple servers trying to write to the file at the same time and failing to report at all.
You'll need to refine your results, of course. If you don't change anything, any machine where you run this will just drop a CSV called "COMPUTERNAME_DATE_AV_FileInfo.csv" at the root of it's C:\ drive.