I'm looking to search the C and E drives of all Windows servers in
Active Directory for any existing copies of putty.exe and their version.
The output needs to have the server name, full path to the executable,
and the file version. So far I have the following code (which right now is only using
two servers for testing:
$ComputerName = Get-ADComputer -filter "name -like 'computer01' -or name `
-like 'server01'" | select -ExpandProperty name
$OutputArr = #()
$findFiles = foreach($computer in $computername){
$file = Invoke-Command -computername $computer { Get-ChildItem -Path `
c:\, e:\ -Recurse | where-object{(!$_.psiscontainer -eq $true) -and `
($_.name -like "putty.exe")} | ForEach-Object -process {$_.fullname} }
$output = $OutputObj = New-Object -TypeName PSobject
$OutputObj | Add-Member -MemberType NoteProperty -Name ComputerName -Value $computer
$OutputObj | Add-Member -MemberType NoteProperty -Name FilePath -Value $file
$OutputArr += $OutputObj
Write-Verbose $OutputObj
}
$OutputArr | fl
The above code outputs the following array:
ComputerName : COMPUTER01
FilePath : {C:\Program Files\PuTTY\putty.exe, C:\Program Files (x86)\PuTTY\PUTTY.EXE}
ComputerName : SERVER01
FilePath : {C:\Program Files (x86)\putty\putty.exe, C:\Users\testuser\Desktop\Public Desktop\putty.exe}
This produces the correct data, but now I need to run another snippet of code against each
separate filepath under computername, but am not sure how to accomplish this, as it is
taking the full filepath with multiple entries.
Essentially, I need to separate each ComputerName in the array into multiple lines:
COMPUTER01,C:\Program Files\PuTTY\putty.exe
COMPUTER01,C:\Program Files (x86)\PuTTY\PUTTY.EXE
SERVER01,C:\Program Files (x86)\putty\putty.exe
Etc...
Is an array not the correct way to do it?
If you are working strictly with what you already have stored in $OutputArr, the following will work:
$out = foreach ($line in $OutputArr) {
if ($line.filepath.count -gt 1) {
foreach ($fp in $line.FilePath) {
[pscustomobject][ordered]#{ComputerName = $line.ComputerName; FilePath = $fp}
}
}
else {
$line
}
}
$out | ConvertTo-Csv -NoTypeInformation
The foreach loop creates new objects with properties ComputerName and FilePath and stores them in $out as an array of objects.
If you do not care about properties and just want a comma-delimited list, you can use the following:
foreach ($line in $OutputArr) {
if ($line.filepath.count -gt 1) {
foreach ($fp in $line.FilePath) {
"{0},{1}" -f $line.ComputerName,$fp
}
}
else {
"{0},{1}" -f $line.ComputerName,$line.FilePath
}
}
This does the same looping as the first solution but instead uses the format operator (-f) to format the output. Piping to ConvertTo-Csv formats the output to be comma-delimited with the properties as headers.
You could move your desired functionality into your code before you even store anything in $OutputArr. I feel like doing all this after all of the other looping to create $OutputArr is just adding inefficiency.
PowerShell can get tricky when doing remote sessions. The below script should be a good starting point for you. Here are some other areas for improvement:
Doing Get-ChildItem -Recurse at the root of a drive will use an inordinate amount of memory and you could cause unintentional page file expansion or even make a server unresponsive due to 100% memory usage. In my snippet below I am using a list of well known paths. If you need to to identify if putty.exe is started on additional machines, your monitoring solution hopefully has process performance data and you can search for putty.exe there.
Speaking of memory management, remote shells have limitations of how much memory they can use. If you run winrm get winrm/config/winrs you will see the upper limit.
If you are going to authenticate to additional resources from within your remote script blocks, you will need to set up authentication that supports double hop scenarios (CredSSP or Kerberos)
$computerNames = #('computer1','computer2')
foreach($computer in $computerNames)
{
<#
First Script Block checks well known paths for putty.exe
#>
$puttyResults = Invoke-Command -ComputerName $computer -ScriptBlock {
$wellKnownPaths = #()
$wellKnownPaths += Join-Path $env:USERPROFILE -ChildPath "Desktop"
$wellKnownPaths += "D:\tools\"
$wellKnownPaths += $env:Path.Split(';')
$puttyPaths = #()
foreach($path in $wellKnownPaths)
{
$puttyPaths += Get-ChildItem $path -Filter "putty.exe" -Recurse
}
if($puttyPaths.Count -gt 0)
{
$resultsArray = #()
foreach($path in $puttyPaths)
{
$resultsArray += [PSCustomObject]#{
ComputerName = $env:COMPUTERNAME
PuttyPath = $path.FullName
}
}
return $resultsArray
}
return $null
}
if($puttyResults -ne $null)
{
foreach($result in $puttyResults)
{
<#
Second script block takes action against putty.exe
#>
$puttyExists = Invoke-Command -ComputerName $computer -ArgumentList #($result.PuttyPath) -ScriptBlock {
Param(
$PuttyPath
)
return (Test-Path $PuttyPath)
}
if($puttyExists)
{
$msg = "Putty exists on '{0}', at '{1}'" -f $result.ComputerName, $result.PuttyPath
Write-Host $msg -ForegroundColor:Yellow
}
}
}
}
I am not sure what exactly you are wanting to do, but this should work for iterating through your custom object. Your Invoke-Command can be simplified also.
$file = Invoke-Command -computername $computer { Get-ChildItem -Path "C:\", "E:\" -Recurse -File -Filter "putty.exe" | Select -Property VersionInfo }
$OutputObj = New-Object -TypeName PSobject
$OutputObj | Add-Member -MemberType NoteProperty -Name ComputerName -Value $env:COMPUTERNAME
$OutputObj | Add-Member -MemberType NoteProperty -Name FilePath -Value $file
$OutputArr += $OutputObj
foreach ($item in $OutputArr)
{
for ($i = 0; $i -lt $item.FilePath.Count; $i++)
{
Write-Output ([string]::Join(', ', $item.ComputerName, $item.FilePath[$i].VersionInfo.FileName, $item.FilePath[$i].VersionInfo.FileVersion))
}
}
I am trying to get my head around Powershell and have the following:
$servers = Get-Content -Path C:\servers.txt
Get-Process -name AVProcess -cn $servers | % where {$_.Status -eq "Running"}
#Export-Csv -path "C:\server-process-sophos.csv"
I get an error, it looks like i cant use the $servers Variable within the parameter computername, also sending to the CSV file is way off. Ideally i'd like to be able to pipe each server name, status of the process into rows in a CSV.
I am trying to return a list of servers which do/dont have that process running or not - so i can see if they have AV installed essentially for an inventory report (this is not good if some dont have AV!!!). How can i best go about this?
Thanks in advance
Get-Process doesn't have a property "Status." It is better/easier to check service status on each machine using Get-Service. You just need to modify service name in this code so it matches your service name. Also output goes to two different files, one is used when service is available for checking, the other is used when there is no service with your needed name. It works with PowerShell V3.
$servers = Get-Content -Path C:\servers.txt
$ErrorActionPreference='stop'
ForEach ($server in $servers) {
try
{
Get-Service -name <enterServiceNameHere> -cn $server | Export-Csv -append -Path "C:\server-process-sophos.csv" -noType -Force
}
catch [System.Management.Automation.ActionPreferenceStopException]
{
$psObject = $null
$psObject = New-Object psobject
$OutputString = "cannot find this service on: " + $server
Add-Member -InputObject $psObject -MemberType noteproperty -Name "List" -Value $OutputString
Export-Csv -InputObject $psObject -Append -path "C:\ServersWithNoService.csv" -NoType -Force
}
}
Hi I am running this script again multiple servers (initially posted here) & I would like to get each specific servers names to be appeared in the result. But right now, I am able to get with the heading CPU & Memory Usage & then the usage for each server one after the other. Pls let me know how to get each server name & the result.
$Output = 'C:\temp\Result.txt'
$ServerList = Get-Content 'C:\temp\ServerNames.txt'
$ScriptBLock = {
$CPUPercent = #{
Label = 'CPUUsed'
Expression = {
$SecsUsed = (New-Timespan -Start $_.StartTime).TotalSeconds
[Math]::Round($_.CPU * 10 / $SecsUsed)
}
}
$MemUsage = #{
Label ='RAM(MB)'
Expression = {
[Math]::Round(($_.WS / 1MB),2)
}
}
Get-Process | Select-Object -Property Name, CPU, $CPUPercent, $MemUsage,
Description |
Sort-Object -Property CPUUsed -Descending |
Select-Object -First 15 | Format-Table -AutoSize
}
foreach ($ServerNames in $ServerList) {
Invoke-Command -ComputerName $ServerNames {Write-Output "CPU & Memory Usage"}
| Out-File $Output -Append
Invoke-Command -ScriptBlock $ScriptBLock -ComputerName $ServerNames |
Out-File $Output -Append
I see you're running with a loop on the servers names ($ServerNames is each server for each iteration), so why don't you use:
"Working on $ServerNames.." | Out-File $Output -Append
on the first line after the "foreach" statement?
Anyway, I think you can change your script like this:
On the script block add:
Write-Output "CPU & Memory Usage"
hostname | out-file $output -Append # this will throw the server name
Once you have this on the Scriptblock, you can run it like this:
Invoke-Command -ScriptBlock $ScriptBLock -ComputerName $ServerList
($ServerList is the original servers' array, which "Invoke-Command" knows how to handle).
I am kinda new to powershell. Only be toying with it for a few days now and have written the below script to help search for multiple conditions in a csv file. I wrote something similar in VB and it takes 2 days to process the csv file. This powershell script takes about 6 hours to process 6500 machines and 9 policies. What i am trying to do is look in Policy.csv for a computer from computers.csv and a policy from a list and report if the computer has it or not.
Policy.csv has 6 fields in the table that need to be in the final report with an additional field added for status of the policy.
Computers.csv has 2 fields in the table that are the computer name and the OU it is in.
Packlist.txt is just the list of the applications(policies) that are being looked for.
Edit:
Samples of the csv files are as follows
Policy.csv
Device,Device DN,Group,Group DN,Policy Domain,Policy
Comp1,OU=Here,Domain_app,OU=Here,Ou=Apps,Server1,Policy1
Comp1,OU=Here,Domain_app,OU=Here,Ou=Apps,Server1,Policy2
Comp1,OU=Here,Domain_app,OU=Here,Ou=Apps,Server1,Policy3
Comp2,OU=There,Domain_app,OU=Here,Ou=Apps,Server1,Policy1
Comp2,OU=There,Domain_app,OU=Here,Ou=Apps,Server1,Policy2
Comp2,OU=There,Domain_app,OU=Here,Ou=Apps,Server1,Policy3
Computer.csv
Device,Device DN
Comp1,OU=Here
Comp2,OU=There
Comp3,OU=AnyWhere
Packlist.txt
Policy1
Policy3
Result.csv
Device,Device DN,Group,Group DN,Policy Domain,Policy,Status
Comp1,OU=Here,Domain_app,OU=Here,Ou=Apps,Server1,Policy1,Entitled
Comp2,OU=There,Domain_app,OU=Here,Ou=Apps,Server1,Policy1,Entitled
Comp3,OU=AnyWhere,,,,Policy1,Notentitled
Comp1,OU=Here,Domain_app,OU=Here,Ou=Apps,Server1,Policy3,Entitled
Comp2,OU=There,Domain_app,OU=Here,Ou=Apps,Server1,Policy3,Entitled
Comp3,OU=AnyWhere,,,,Policy3,Notentitled
The code is:
$data=import-csv -path c:\packagestatus\policy.csv
$computers=import-csv -path c:\packagestatus\computers.csv
$policylist= (Get-content -path c:\packagestatus\packlist.txt)
$policycount = $Policylist.count
$computercount = $computers.count
$Policycounter = 1
foreach ($policy in $policylist)
{
$Policy
$host.ui.RawUI.WindowTitle = "Processing $policyCounter of $policycount"
$Data_temp = $data|where-object{$_."Policy Instance" -eq $policy}
$computercounter = 1
foreach ($Computer in $computers)
{
$host.ui.RawUI.WindowTitle = "Processing Policy $policyCounter of $policycount and Computer $computercounter of $computercount"
if ($data_temp|Where-Object{$_.Device -eq $computer.device})
{
$result = $data_temp|where-object{$_.Device -eq $computer.device}|Where-Object{$_."Policy Instance" -eq $policy}
$result|Add-member -membertype Noteproperty -name Status -value Entitled
$result|export-csv -path c:\packagestatus\result1.csv -NoTypeInformation -append
}
Else
{
$result1 = New-Object -TypeName PSObject
$result1|add-member -membertype noteproperty -name "Device" -value $computer.device
$result1|add-member -membertype noteproperty -name "Device DN" -value $computer."Device DN"
$result1|add-member -membertype noteproperty -name "Group" -value $null
$result1|add-member -membertype noteproperty -name "Group DN" -value $null
$result1|add-member -membertype noteproperty -name "Policy Domain" -value $null
$result1|add-member -membertype noteproperty -name "Policy Instance" -value $Policy
$result1|add-member -membertype noteproperty -name "Status" -value NotEntitled
$result1|export-csv -path c:\packagestatus\result1.csv -force -NoTypeInformation -append
}
$computercounter++
}
$policycounter++
}
$host.ui.RawUI.WindowTitle = "Completed"
Ok, I think this should run faster for you...
I start by making a function to create objects for a given computer name, DN, and policy that it's missing. Then I load up the data from the files. Next I make a regex string to match against for all of the policies that are in the $Policylist. I do the same for the computer list. Then I filter down the $Data array for only entries that are in the policy list and also in the computer list.
This will, hopefully, limit the data that we're dealing with, and I think that will be faster in general. Next I group it by device, and for each grouping I look for any missing policies, and run that list against the function, and any matching policies I add the 'Status' property and output that entry. This is all collected in the $Results array.
Once we process all the computers we have records for, we look for the computers that weren't in the list, and create a NotEntitled object for all policies, and all all those to the $Results.
Lastly we sort and output $Results. It would be faster to not sort it I suppose, but probably harder to read as well. Here's the code, let me know how it works out for you:
Function NotEntitled{
[CmdletBinding()]
Param(
[String]$Device,
[String]$DeviceDN,
[String[]]$Pack
)
Process{
ForEach($Item in $Pack){
[PSCustomObject]#{
'Device' = $Device
'Device DN' = $DeviceDN
'Group' = $null
'Group DN' = $null
'Policy Domain' = $null
'Policy' = $Item
'Status' = 'NotEntitled'
}
}
}
}
$Data = import-csv -path c:\packagestatus\policy.csv
$Computers = import-csv -path c:\packagestatus\computers.csv
$Policylist = ,(Get-content -path c:\packagestatus\packlist.txt)
$PolicyReg = ($Policylist|%{[regex]::Escape($_)}) -join '|'
$ComputerReg = ($Computers.Device|%{[regex]::Escape($_)}) -join '|'
$FilteredData = $Data | Where{$_.Policy -match $PolicyReg -and $_.device -match $ComputerReg}
$Results = $FilteredData | Group Device | ForEach{
$Device = $_.group
$MissingPolicies = ,($Policylist | Where{$_ -notin $Device.Policy})
If(![string]::IsNullOrEmpty($MissingPolicies)){NotEntitled $Device[0].Device $Device[0].'Device DN' $MissingPolicies}
$Device | ForEach{Add-Member -InputObject $_ -NotePropertyName 'Status' -NotePropertyValue 'Entitled' -PassThru}
}
$CompList = $FilteredData | Select -ExpandProperty Device -Unique
$Results += $Computers | Where{$_.Device -notin $CompList} | ForEach{NotEntitled $_.Device $_.'Device DN' $Policylist}
$Results | Sort Device,Policy | Export-Csv c:\packagestatus\Result.csv -NoTypeInformation
I took your sample data, changed Comp1 Policy3 to Comp1 Policy4 (so that I could have a computer with only a partial policy set), and ran it and got these results output:
"Device","Device DN","Group","Group DN","Policy Domain","Policy","Status"
"Comp1","OU=Here","Domain_app","OU=Here,Ou=Apps","Server1","Policy1","Entitled"
"Comp1","OU=Here",,,,"Policy3","NotEntitled"
"Comp2","OU=There","Domain_app","OU=Here,Ou=Apps","Server1","Policy1","Entitled"
"Comp2","OU=There","Domain_app","OU=Here,Ou=Apps","Server1","Policy3","Entitled"
"Comp3","OU=AnyWhere",,,,"Policy1","NotEntitled"
"Comp3","OU=AnyWhere",,,,"Policy3","NotEntitled"
Quick question, I wanted to be able to pull a large list of server and spit the OS type with the server name into a CSV file. I have gotten this script to do that for the most part, however I am running into a few issues.
The script fails unless I have the columns already in the csv, I have a feeling im using the append flag incorrectly.
The list in question could contain Linux, Sun system, older then win 2003 and other non windows system. As a result error are going to happen, currently it just skips a line in the csv. Is there a way to write the server name and the word failed in the Os filed? Or just disable errors in general?
Thanks in advance for all your help!
$servers = Get-Content C:\Automation\Servers.txt
Foreach ($s in $servers)
{
$OSInfo = Get-WmiObject Win32_OperatingSystem -ComputerName $s #Get OS Information
$infoObject = New-Object PSObject
Add-Member -inputObject $infoObject -memberType NoteProperty -name "ServerName" -value $CPUInfo.SystemName
Add-Member -inputObject $infoObject -memberType NoteProperty -name "OS_Name" -value $OSInfo.Caption
$infoObject | Export-Csv -NoTypeInformation -Append -Path C:\Automation\test.csv
}
If you use a try-catch you can more easily handle error situations, so I would start to examine the following changes to the code:
$servers = Get-Content C:\Automation\Servers.txt
$output = #()
foreach ($s in $servers)
{
$infoObject = New-Object PSObject
try {
$OSInfo = Get-WmiObject Win32_OperatingSystem -ComputerName $s
Add-Member -inputObject $infoObject -memberType NoteProperty -name 'ServerName' -value $s
Add-Member -inputObject $infoObject -memberType NoteProperty -name 'OS_Name' -value $OSInfo.Caption
}
catch {
Add-Member -inputObject $infoObject -memberType NoteProperty -name 'ServerName' -value $s
Add-Member -inputObject $infoObject -memberType NoteProperty -name 'OS_Name' -value 'Non-Windows'
}
$output += $infoObject
}
$output | Export-Csv -NoTypeInformation -Path c:\Automation\test.csv
One of the changes here, besides the try-catch, is that I create an empty array, and collect all the infoObjects in this, and just do one export-csv in the end. That way you shouldn't get the problem with the csv. I also use the $s for servername so that you will get the servername even though the WMI query fails. You also get 'Non-Windows' for any server were WMI fails. You could perhaps change this to 'WMI Failed' or something if you want to, as it might also fail on windows servers of course.
NOTE! I haven't actually tested this code, but it should give you something to work on. There are other stuff you could look into to optimize it a bit as well, such as using a WMI -query, and there is also easier ways of creating a PSObject, but since yours just have two properties it won't matter as much.
Hope this helps. Good luck.
You can set $ErrorActionPreference = 'SilentlyContinue' to make Get-WmiObject ignore errors. Also, the -ComputerName parameter accepts an array of hostnames, so you don't need to run the cmdlet in a loop and append to the output file multiple times.
$serverlist = 'C:\Automation\Servers.txt'
$oslist = 'C:\Automation\test.csv'
$servers = Get-Content $serverlist
$ErrorActionPrefSave = $ErrorActionPreference
$ErrorActionPreference = 'SilentlyContinue'
$OSInfo = Get-WmiObject -Class Win32_OperatingSystem -Computer $servers |
select #{n='Computer';e={$_.__SERVER}},
#{n='OS_Name';e={$_.Caption}}
$ErrorActionPreference = $ErrorActionPrefSave
# write responses to output file
$OSInfo | Export-Csv $oslist -NoType
# filter hosts that responded from server list and append the remaining hosts
# (the ones that did not respond) with a failure notice to the output file
$servers | ? { $OSInfo.Computer -notcontains $_ } |
select #{n='Computer';e={$_}}, #{n='OS_Name';e={'connection failed'}} |
Export-Csv $oslist -NoType -Append
Note that $OSInfo.Computer -notcontains $_ requires PowerShell v3 or newer. For earlier PowerShell versions you need to change that to ($OSInfo | select -Expand Computer) -notcontains $_.