Assume I have an array of Windows hostnames:
$server_hostnames = #("server1","server2")
For each server I need to find if there are any sessions for a specific user, and then ideally capture all the sessions in a new array.
$results = #()
$account = "JohnDoe"
foreach ($s in $server_hostnames) {
$r = (qwinsta /server:$($s) 2> $null | out-string -stream | sls $account)
$results += $r
}
In this example I have 2 servers but in production I would run it against 2-3000 servers, so parallel execution is extremely important.
I have made several attempts at rewriting the code using jobs, workflows, or Split-Pipeline, but with little success.
Usually, filtering with sls (Select-String) doesn't work, not even with findstr.
Split-Pipeline example:
PS C:\SplitPipeline> $server_hostnames | Split-Pipeline {process{ $_; qwinsta /server:$($_) | out-string -stream | sls $account }}
Split-Pipeline : Cannot bind argument to parameter 'Pattern' because it is null.
At line:1 char:21
+ $server_hostnames | Split-Pipeline {process{ $_; qwinsta /server:$($_) | out-str ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidData: (:) [Split-Pipeline], ParameterBindingValidationException
+ FullyQualifiedErrorId : ParameterArgumentValidationErrorNullNotAllowed,SplitPipeline.SplitPipelineCommand
Using jobs you could do something like this:
$servers = 'server1', 'server2'
$account = 'JohnDoe'
$results = #()
$sb = {
Param($server, $name)
& qwinsta /server:$server |
Select-String $name -SimpleMatch |
Select-Object -Expand Line
}
$servers | ForEach-Object {
Start-Job -ScriptBlock $sb -ArgumentList $_, $account | Out-Null
}
$results += while (Get-Job -State Running) {
Get-Job -State Complete | ForEach-Object {
Receive-Job -Job $_
Remove-Job -Job $_
}
}
$results += Get-Job -State Complete | ForEach-Object {
Receive-Job -Job $_
Remove-Job -Job $_
}
However, since you want to run qwinsta against several thousand servers you'll probably want to use a job queue rather than running all jobs at the same time, otherwise the massive number of jobs might exhaust your local computer's resources.
Related
I am trying to get a script working to audit folder permissions on a Windows server, among other data, and export this data to a CSV file for analysis after a ransomware attack.
I ripped the script from a forum, but it did not run correctly as is. Below is a slightly modified version during my troubleshooting.
I am well versed in batch scripting, and have a decent understanding of loops and pipelining, but this Powershell script has me scratching my head.
It seems like the array is not making it to the nested loop.
I am testing in Windows 10 Pro 21H1, using Powershell version 5.1.19041.1320, build 10.0.19041.1320
##The script:
$ErrorActionPreference = "Continue"
$strComputer = $env:ComputerName
$colDrives = Get-PSDrive -PSProvider Filesystem
ForEach ($DriveLetter in $colDrives) {
$StartPath = "$DriveLetter`:\"
Get-ChildItem -LiteralPath $StartPath -Recurse | ?{ $_.PSIsContainer } |
ForEach ($FullPath = Get-Item -LiteralPath{Get-Item -LiteralPath $_.PSPath}{Get-Item -
LiteralPath $FullPath}.Directoryinfo.GetAccessControl())}
Select #{N='Server Name';E={$strComputer}}
#{N='Full Path';E={$FullPath}}
#{N='Type';E={If($FullPath.PSIsContainer -eq $True) {'D'} Else {'F'}}}
#{N='Owner';E={$_.Owner}}
#{N='Trustee';E={$_.IdentityReference}}
#{N='Inherited';E={$_.IsInherited}}
#{N='Inheritance Flags';E={$_.InheritanceFlags}}
#{N='Ace Flags';E={$_.PropagationFlags}}
#{N='Ace Type';E={$_.AccessControlType}}
#{N='Access Masks';E={$_.FileSystemRights}}
Export-CSV -NoTypeInformation -Delimiter "|" -Path "$strComputer`_$DriveLetter.csv"
##The error I am getting:
You cannot call a method on a null-valued expression.
At C:\Users\user\Documents\fileaudit2.ps1:8 char:13
ForEach ($FullPath = Get-Item -LiteralPath{Get-Item -LiteralPath $ ...
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
CategoryInfo : InvalidOperation: (:) [], RuntimeException
FullyQualifiedErrorId : InvokeMethodOnNull
##when I modify the nested loop as follows:
ForEach ($FullPath = Get-Item -LiteralPath{Get-Item -LiteralPath $_.PSPath}{Get-Item -LiteralPath $FullPath}).Directoryinfo.GetAccessControl()}
##I get the error:
Get-Item : Cannot evaluate parameter 'LiteralPath' because its argument is specified as a script block and there is no input. A script block cannot be evaluated without
input.
At C:\Users\user\Documents\fileaudit2.ps1:8 char:46
... Path = Get-Item -LiteralPath{Get-Item -LiteralPath $_.PSPath}{Get-Ite ...
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
CategoryInfo : MetadataError: (:) [Get-Item], ParameterBindingException
FullyQualifiedErrorId : ScriptBlockArgumentNoInput,Microsoft.PowerShell.Commands.GetItemCommand
##I'm just wholly struggling to understand what is not working in this loop.
You are mixing a lot of unneeded Get-Item calls in there.
I also would not use Get-PSDrive for this because I assume you don't want to get results for CD drives, USB devices etc in the report.
Try:
# this returns drives WITH a trailing backslash like C:\
$colDrives = ([System.IO.DriveInfo]::GetDrives() | Where-Object { $_.DriveType -eq 'Fixed' }).Name
# or use:
# this returns drives WITHOUT trailing backslash like C:
# $colDrives = (Get-CimInstance -ClassName win32_logicaldisk | Where-Object { $_.DriveType -eq 3 }).DeviceID
$result = foreach ($drive in $colDrives) {
Get-ChildItem -LiteralPath $drive -Directory -Recurse -ErrorAction SilentlyContinue |
ForEach-Object {
$path = $_.FullName
$acl = Get-Acl -Path $path
foreach ($access in $acl.Access) {
[PsCustomObject]#{
Server = $env:COMPUTERNAME
Drive = $drive[0] # just the first character of the drive
Directory = $path
Owner = $acl.Owner
Trustee = $access.IdentityReference
Inherited = $access.IsInherited
InheritanceFlags = $access.InheritanceFlags -join ', '
'Ace Flags' = $access.PropagationFlags -join ', '
'Ace Type' = $access.AccessControlType
'Access Masks' = $access.FileSystemRights -join ', '
}
}
}
}
# now you can save your result as CSV file for instance you can double-click to open in Excel:
$result | Export-Csv -Path 'X:\WhereEver\audit.csv' -NoTypeInformation -UseCulture
To do this on several remote machines, wrap it inside Invoke-Command
# set the credentials for admin access on the servers
$cred = Get-Credential 'Please enter your admin credentials'
# create an array of the servers you need to probe
$servers = 'Server01', 'Server02'
$result = Invoke-Command -ComputerName $servers -Credential $cred -ScriptBlock {
$colDrives = ([System.IO.DriveInfo]::GetDrives() | Where-Object { $_.DriveType -eq 'Fixed' }).Name
foreach ($drive in $colDrives) {
# code inside this loop unchanged as above
}
}
# remove the extra properties PowerShell added
$result = $result | Select-Object * -ExcludeProperty PS*, RunspaceId
# output to csv file
$result | Export-Csv -Path 'X:\WhereEver\audit.csv' -NoTypeInformation -UseCulture
I have a function (which started out as a ScriptBlock) and I'd like to be able to splat my input parameters to store configurations, but instead I'm passing each of them individually and this is bad, because I'd rather just pass the hashmap when I want to run the script block using Invoke-Command -Command ${function:sb} -ArgumentList $service_host_download_11_1_1
or
Invoke-Command -Command ${function:sb} -ArgumentList $service_host_11_1_1
But thus far I have been unable to achieve this; see the code below, is there a way to do this?
# I'd like to be able to splat these to my sb function
$service_host_download_11_1_1 = #{
packageName = 'ServiceHostDownload'
searchPattern = 'VERSION,'
host_arr = $(Get-ADComputer -LDAPFilter '(&(name=MCDONALDS*)(!(name=MCDONALDSTAB*)))' -SearchBase "OU=Simphony,OU=MCDONALDS,OU=Florda,OU=Places,DC=mckeedees,DC=com" | Select-Object -ExpandProperty Name)
}
# And this also
$service_host_11_1_1 = #{
packageName = 'ServiceHost'
searchPattern = 'VERSION,|REBOOT'
host_arr = $(Get-ADComputer -LDAPFilter '(&(name=MCDONALDS*)(!(name=MCDONALDSTAB*)))' -SearchBase "OU=Simphony,OU=MCDONALDS,OU=Florda,OU=Places,DC=mckeedees,DC=com" | Select-Object -ExpandProperty Name)
}
function sb($host_arr,
$packageName,
$searchPattern) {
$host_arr | % {
Invoke-Command -ComputerName $_ -AsJob -ArgumentList #($packageName, $searchPattern) -ScriptBlock {param($pn, $sp)
$result = gc -path "C:\Micros\Simphony\CALTemp\Packages\$($pn)\Setup_log.txt" | Select-String -Pattern $sp
Write-Host "$($ENV:COMPUTERNAME),$($pn),$($result)"
}
}
}
# I'd rather be splitting with Invoke-Command here.
sb $service_host_download_11_1_1['host_arr'] $service_host_download_11_1_1['packageName'] $service_host_download_11_1_1['searchPattern']
# Wait for completion
Start-Sleep -Seconds 5000
Get-Job | ? { $_.State -eq 'Completed'} | Receive-Job
# Write out failed
#Get-Job | ? { $_.State -eq 'Failed'} | Receive-Job
# Set us up for next time...
Get-Job | Remove-Job
I have a script that checks remote servers for tomcat and the associated java versions. It takes about 60 seconds to run against a list of about 16 servers. I'm just curious if the script is as efficient as realistically possible. I'm far from a PowerShell pro but I'm satisfied with the outcome. Just checking for where there is room for improvement.
$Servers = 'server1','server2','etc'
$Output = #()
foreach ($Server in $Servers)
{
$SName = gwmi -Class Win32_Service -ComputerName $Server -Filter {Name LIKE 'Tomcat%'}
IF ($SName -ne $null) {
$Output += [PSCustomObject]#{
Server_name = $SName.PSComputerName
Service_name = $SName.Name
Service_status = $SName.State
Tomcat_version = "$(Get-Content -Path ("\\"+$SName.PSComputerName+"\"+"$($SName.PathName.ToString())".Substring(0,$SName.Pathname.LastIndexOf("\")-3)+"\webapps\ROOT\RELEASE-NOTES.txt" -replace ":", "$") | Select-String -Pattern 'Apache Tomcat Version ')".TrimStart()
Java_Version = (Invoke-Command -ComputerName $Server -ScriptBlock {(GCI -Path "$((Get-ItemProperty -Path 'HKLM:\SOFTWARE\WOW6432Node\Apache Software Foundation\Procrun 2.0\Tomcat9\Parameters\Java').Jvm)").VersionInfo.ProductName})
}
}
Else {}
}
$Output | Select Server_name, Service_name,Service_status, Tomcat_Version, Java_Version | Format-Table -AutoSize
Can I simplify things anymore?
Is the time to completion decent for what is being performed?
Invoke-Command allows you to connect with multiple computers at the same time which should be more efficient.
+= is pretty bad, please read: Why should I avoid using the increase assignment operator (+=) to create a collection
You're querying WMI first and then if service is there you are using Invoke-Command, mind as well, connect once to the remote host and check everything.
I personally would do something like this
$Servers = 'server1','server2','etc'
$scriptBlock = {
# Since you're querying each server, and then if the service is there you Invoke-Command,
# mind as well Invoke-Command at first and if the service is there enter the If condition,
# else close the connection.
$tomcatServ = Get-CimInstance -Class Win32_Service -Filter "Name LIKE 'Tomcat%'"
if($tomcatServ)
{
##### This part is pretty confusing for someone reading your code, if you show us how does
##### RELEASE-NOTES.txt looks we may be able to improve it and simplified it a bit
$path = $tomcatServ.PathName.Substring(0,$tomcatServ.PathName.LastIndexOf("\")-3)
$path = Join-Path $path -ChildPath "webapps\ROOT\RELEASE-NOTES.txt"
$tomCatVer = ((Get-Content $path) -replace ":", "$" | Select-String -Pattern 'Apache Tomcat Version ').TrimStart()
##### This part is a bit confusing too
$key = 'HKLM:\SOFTWARE\WOW6432Node\Apache Software Foundation\Procrun 2.0\Tomcat9\Parameters\Java'
$javaVer = GetChild-Item -Path ((Get-ItemProperty -Path $key).Jvm).VersionInfo.ProductName
#####
[PSCustomObject]#{
Server_name = $env:ComputerName
Service_name = $tomcatServ.Name
Service_status = $tomcatServ.State
Tomcat_version = $tomCatVer
Java_Version = $javaVer
}
}
}
$Output = Invoke-Command -ComputerName $Servers -ScriptBlock $scriptBlock -HideComputerName
$Output | Select-Object * -ExcludeProperty RunspaceID | Format-Table -AutoSize
How do I properly use $_ in out-file? Here's my code:
get-content computers.txt |
Where {$_ -AND (Test-Connection $_ -Quiet)} |
foreach { Get-Hotfix -computername $_ } |
Select CSName,Description,HotFixID,InstalledBy,InstalledOn |
convertto-csv | out-file "C:\$_.csv"
I'm trying to execute a get-hotfix for all the computers listed in the text file then I want them to be exported to CSV with the computer name as the filename.
You need one pipeline to process the computers.txt files, and a nested one inside the foreach to process the list of hotfixes for each computer:
get-content .\computers.txt |
Where {$_ -AND (Test-Connection $_ -Quiet)} |
foreach {
Get-Hotfix -computername $_ |
Select CSName,Description,HotFixID,InstalledBy,InstalledOn |
convertto-csv | out-file "C:\$_.csv"
}
Edit: Changed computers.txt to .\computers.txt, as this is required for local paths in powershell
i can see with this:
get-content .\computers.txt | Where {$_ -AND (Test-Connection $_ -Quiet)} | foreach{ Get-Hotfix -id KB4012212 -computername $_ | Select CSName,Description,HotFixID,InstalledBy,InstalledOn | convertto-csv | out-file "C:\$_.csv" }
i can see only in which PC is the fix (KB4012212) installed.
it's possible to see the following
CSNAME Fix(Inst/NotInst)
PC1 FIxInstalled
PC2 FixNotinstalled
PC3 FixnotInstalled
..
..
etc
I monkeyed with this for a while and nothing I found on-line worked until I used this combo.
I used the method is this thread but it was SO slow and I wanted to learn more about using jobs so this is what ended up working for me on Windows 7 PS Ver 4.
All other options were either too slow or did not return data from the remote system.
$VMs = get-content C:\WinVms.txt #Generate your hostnames list however you deem best.
foreach ($vm in $vms)
{
Write-Host "Attempting to get hotfixes on:" $vm
invoke-command -computername $vm -ScriptBlock {start-job -scriptblock {(get-hotfix | sort installedon)[-1]} | wait-job | receive-job} -AsJob
}
start-sleep 60 # give it a minute to complete
get-job | ? { $_.state -eq "Completed"} | receive-job -keep | export-csv c:\temp\win-patch.csv
you can check your failures too like this:
get-job | ? { $_.state -eq "Failed"}
I have the following code that works in that it creates multiple jobs and runs what's inside the scriptblock on all of the computers in the array ($SMSMembers). The problem is that it doesn't give any sort of meaningful output back that I can use to determine if the code ran successfully or not. I have tried about 100 different things that I have Googled but none of the solutions seemed to work for me. This is the code I'm trying to run that I thought should work according to other posts I've seen on StackOverflow.
$SMSMembers = #("computer1","computer2","computer3")
$output = #()
foreach ($compName in $SMSMembers) {
$scriptblock = {
$file = {"test"}
$filepath = "\\$using:compName\c$\scripts\NEWFILE.txt"
$file | Out-File -FilePath $filepath
Start-Sleep -Seconds 5
Remove-Item $filepath
}
$output += Start-Job -ScriptBlock $scriptblock | Get-Job | Receive-Job
}
Get-Job | Wait-Job
foreach ($item in $output) {
Write-Host $item
}
This script doesn't do much except copy a file to a remote computer and then delete it. I would just like to get output if the job was successful or not. Like I said this code works like it should, I just don't get feedback.
My end goal is to be able to send a command to an array of computers ($SMSMembers) and request the current user with this code and get the username input back:
$user = gwmi Win32_ComputerSystem -Comp $compName |
select Username -ExpandProperty Username
You create the job, get the job info, and then receive the job back to back to back, before the job can complete. Instead, collect the job info, then outside the loop wait for the jobs to finish, and receive the output when the jobs are done.
$SMSMembers = #("computer1","computer2","computer3")
$scriptblock = {
$file = {"test"}
$filepath = "\\$using:compName\c$\scripts\NEWFILE.txt"
$file | out-file -FilePath $filepath
Start-Sleep -Seconds 5
remove-item $filepath
}
$Jobs = foreach($compName in $SMSMembers){
Start-Job -ScriptBlock $scriptblock
}
Wait-Job $Jobs
$Output = Receive-Job $Jobs
foreach ($item in $output){
write-host $item
}
Edit: Modified the script slightly so I wasn't randomly copying files around, but it should still function the same. Then tested it with the expected results:
$SMSMembers = #("computer1","computer2","computer3")
$scriptblock = {
$RndDly=Get-Random -Minimum 10 -Maximum 45
start-sleep -Seconds $RndDly
"Slept $RndDly, then completed for $using:compname"
}
$Jobs = foreach($compName in $SMSMembers){
Start-Job -ScriptBlock $scriptblock
}
Wait-Job $Jobs
$Output = Receive-Job $Jobs
foreach ($item in $output){
write-host $item
}
Id Name PSJobTypeName State HasMoreData Location Command
-- ---- ------------- ----- ----------- -------- -------
1 Job1 BackgroundJob Completed True localhost ...
3 Job3 BackgroundJob Completed True localhost ...
5 Job5 BackgroundJob Completed True localhost ...
Slept 30, then completed for computer1
Slept 27, then completed for computer2
Slept 11, then completed for computer3
Look at the below code and i think you should be able to figure this out.
> Get-job | Receive-Job 2>&1 >> c:\output.log
The 2>&1 >> with collect all output from Get-Job | Receive-Job it should work similarly for start-job