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 had this script, but the format was in HTML and I cleaned up all the code and changed the commands that were "gwmi" to "Get-CimInstance" to have good practices. My goal is to transform this script that handles a hostname to a list of hostnames.
Can someone help me?
The idea I had would be to have a window that has a "browse" button to import the list, be it in txt or csv and for each hostname in that list it would do these commands and in the end it would export to an xlsx file (I tried with Export-Excel, but you need to download the module separately, and you need to trust the repository, authorize the import of the module for later use, so I would have to make this standalone, without any request, because I would convert this ps1 into an exe file) and the data, would need to be side by side with the headers, e.g. Hostname, Last User Logged, Type Of Chassis etc.
I would be very grateful if someone can help me, I've been building part 1 of this script for a few hours now, and now I need to go to part 2 (that is this process to create and export csv results) which is the part where I feel stuck.
Add-Type -AssemblyName Microsoft.VisualBasic
$ComputerName = [Microsoft.VisualBasic.Interaction]::InputBox("Insert the hostname Name", "Hardware Report")
If ($ComputerName -eq "")
{
break
}
#Check computer online in network, if is not online, the hostname will be skipped but necessary add in log entry which computers is offline
$ProgressPreference = 'SilentlyContinue'
try {
$ErrorActionPreference = "Stop";
$TestComputerHost = Test-Connection $ComputerName -Count 1 -InformationAction Continue -WarningAction SilentlyContinue;
} catch {
#Hostname will be skipped
} finally {
$ProgressPreference = 'Continue'
$ErrorActionPreference = "Continue"
}
#Validade crucial service that is crucial for get remote data, and if is not possible to get this information, the hostname will be skipped
try {
$ErrorActionPreference = "Stop"
Get-CimInstance win32_operatingsystem -ComputerName $ComputerName | Out-Null }
catch [System.Runtime.InteropServices.COMException]
{
#Hostname will be skipped
}
#Validate if the WS Management service is enabled on the remote device
$ProgressPreference = 'SilentlyContinue'
$TestComputerHost = Test-NetConnection $ComputerName -Port 5985 -InformationLevel Quiet -WarningAction SilentlyContinue
If ($TestComputerHost -ne "False"){
}
$ProgressPreference = 'Continue'
#Function to create the Get-WUChassisType that is performed to find out if the Chassis of the equipment is Notebook or Desktop, and it is not configured to detect virtual machine
Function Get-WUChassisType {
[CmdletBinding()]
param (
)
Set-StrictMode -Version 'Latest'
[int[]]$chassisType = try {
$ErrorActionPreference = "Stop";
Get-CimInstance Win32_SystemEnclosure -ComputerName $ComputerName | Select-Object -ExpandProperty ChassisTypes;
} catch {
#Here need to be blank result or skip this result but keep the rest results
} finally {
$ErrorActionPreference = "Continue";
}
switch ($chassisType) {
{ $_ -in 3, 4, 5, 6, 7, 15, 16 } {
return 'Desktop'
}
{ $_ -in 8, 9, 10, 11, 12, 14, 18, 21, 31, 32 } {
return 'Notebook'
}
{ $_ -in 30 } {
return 'Tablet'
}
{ $_ -in 17, 23 } {
return 'Servidor'
}
Default {
}
}
}
#Function to get last logged user on remote computer
Function Get-LastUser {
try {
$ErrorActionPreference = "Stop"
Get-WmiObject Win32_LoggedOnUser -ComputerName $ComputerName |
Select Antecedent -Unique |
% {
$domain = $_.Antecedent.Split('"')[1]
if($domain -eq "DOMAIN") {
"{0}\{1}" -f $domain, $_.Antecedent.Split('"')[3]
}
} | Select-Object -First 1
} catch [System.Runtime.InteropServices.COMException]
{
}
}
#Name of remote computer
$Name = 'Hostname' + $ComputerName
#Get last logged user (by function)
$LastLoggedUser = ((Get-LastUser).Split('\')[1])
#Last Boot Time
$LastBoot = (Get-CimInstance win32_operatingsystem -ComputerName $ComputerName |Select-Object CSName, LastBootUpTime | Select -ExpandProperty LastBootUpTime).tostring("dd/MM/yyyy hh:mm:ss")
#Chassis Type Of Computer
$ChassisType = Get-WUChassisType
#Operating System
$OS = (Get-CimInstance win32_operatingsystem -ComputerName $ComputerName).caption
#System Installed On
$SystemInstalledOn = ((Get-CimInstance Win32_OperatingSystem -ComputerName $ComputerName).InstallDate).tostring("dd/MM/yyyy hh:mm:ss")
#Processor
$Processor = (Get-CimInstance win32_processor -ComputerName $ComputerName -filter "deviceid='CPU0'").Name
#Disk
$Disk = (Get-CimInstance Win32_LogicalDisk -ComputerName $ComputerName | Select-Object #{Name="Size"; Expression={"$([math]::round($_.Size / 1GB,2))GB"}}).Size
#Ram Memory
$Ram = (Get-CimInstance Win32_PhysicalMemory -ComputerName $ComputerName | Select-Object #{Name="Capacity"; Expression={"$([math]::round($_.Capacity / 1GB,2))GB"}}).Capacity
#Serial Number
$SerialNumber = Get-CimInstance win32_bios -ComputerName $ComputerName | Select-Object -ExpandProperty SerialNumber
#Manufacturer
$Manufacturer = Get-CimInstance win32_ComputerSystemProduct -ComputerName $ComputerName | Select-Object -ExpandProperty Vendor
#Model
$Model = Get-CimInstance win32_ComputerSystemProduct -ComputerName $ComputerName | Select-Object -ExpandProperty Name
#Export to CSV
$Name + $LastLoggedUser + $ChassisType + $LastBoot + $OS + $SystemInstalledOn + $Processor + $Disk + $Ram + $SerialNumber + $Manufacturer + $Model | Export-Csv
#Dialog box to information finish script
[Microsoft.VisualBasic.Interaction]::MsgBox("Report is finished", "OKOnly,SystemModal,Information", "Success") | Out-Null```
edit: rewrote it a little for you, try the below.
Note that the input csv expects a header called "ComputerName" and a list of computer names underneath that.
I copied the csv-to-excel part at the bottom from here
#Function to let user select a file then return the filepath.
Function Get-FileName($initialDirectory){
[System.Reflection.Assembly]::LoadWithPartialName(“System.windows.forms”) | Out-Null
$OpenFileDialog = New-Object System.Windows.Forms.OpenFileDialog
$OpenFileDialog.initialDirectory = $initialDirectory
$OpenFileDialog.filter = “CSV Exports (*.csv)| *.csv”
$OpenFileDialog.ShowDialog() | Out-Null
$OpenFileDialog.FileName
}
$filepath = Get-FileName -initialDirectory $PSScriptRoot
$csvdata = Import-Csv -Path $filepath
#Function to create the Get-WUChassisType that is performed to find out if the Chassis of the equipment is Notebook or Desktop, and it is not configured to detect virtual machine
Function Get-WUChassisType {
[CmdletBinding()]
param ($ComputerName=$null)
Set-StrictMode -Version 'Latest'
$chassisTypes = Get-CimInstance Win32_SystemEnclosure -ComputerName $ComputerName | Select -ExpandProperty ChassisTypes
switch ($chassisTypes) {
{ $_ -in 3, 4, 5, 6, 7, 15, 16 } {
return 'Desktop'
}
{ $_ -in 8, 9, 10, 11, 12, 14, 18, 21, 31, 32 } {
return 'Notebook'
}
{ $_ -in 30 } {
return 'Tablet'
}
{ $_ -in 17, 23 } {
return 'Servidor'
}
Default {
return ''
}
}
}
#Function to get last logged user on remote computer
Function Get-LastUser($ComputerName){
try {
Get-WmiObject Win32_LoggedOnUser -ComputerName $ComputerName |
Select Antecedent -Unique |
% {
$domain = $_.Antecedent.Split('"')[1]
if($domain -eq "VLINET") {
"{0}\{1}" -f $domain, $_.Antecedent.Split('"')[3]
}
} | Select-Object -First 1
} catch [System.Runtime.InteropServices.COMException]{
}
}
#We'll append all our individual pc resuls into this array
$exportObj = #()
$offlineObj = #()
#Check computer online in network, if is not online, the hostname will be skipped but necessary add in log entry which computers is offline
foreach ($row in $csvdata){
if(Test-Connection $row.ComputerName -Count 1){
$ciminfo = Get-CimInstance win32_operatingsystem -ComputerName $row.ComputerName -ErrorAction SilentlyContinue |Select-Object CSName, LastBootUpTime, Caption, InstallDate
$sysinfo = Get-CimInstance win32_ComputerSystemProduct -ComputerName $row.ComputerName -ErrorAction SilentlyContinue | Select-Object Vendor, Name
if ($ciminfo -and $sysinfo){
$objPcResult = New-Object PSObject -Property #{
Name = 'Hostname: ' + $row.ComputerName;
ChassisType = Get-WUChassisType -ComputerName $row.ComputerName;
LastLoggedUser = ((Get-LastUser -ComputerName $row.ComputerName).Split('\')[1]);
LastBoot = $ciminfo.LastBootUpTime.tostring("dd/MM/yyyy hh:mm:ss");
OS = $ciminfo.Caption;
SystemInstalledOn = $ciminfo.InstallDate.tostring("dd/MM/yyyy hh:mm:ss");
Processor = (Get-CimInstance win32_processor -ComputerName $row.ComputerName -filter "deviceid='CPU0'").Name;
Disk = (Get-CimInstance Win32_LogicalDisk -ComputerName $row.ComputerName | Select-Object #{Name="Size"; Expression={"$([math]::round($_.Size / 1GB,2))GB"}}).Size;
Ram = (Get-CimInstance Win32_PhysicalMemory -ComputerName $row.ComputerName | Select-Object #{Name="Capacity"; Expression={"$([math]::round($_.Capacity / 1GB,2))GB"}}).Capacity;
SerialNumber = Get-CimInstance win32_bios -ComputerName $row.ComputerName | Select-Object -ExpandProperty SerialNumber;
Manufacturer = $sysinfo.Vendor;
Model = $sysinfo.Name;
}
#Add each PC results as a new row in our array
$exportObj += $objPcResult
}
}else{
$objPcResult = New-Object PSObject -Property #{
Name = 'Hostname: ' + $row.ComputerName;
}
$offlineObj += $objPcResult
}
}
#Setup our temp variables to save our collected data as a temporary CSV, so we can import it into Excel to save as an XLSX.
$offlinecsv = "c:\temp\offline.csv" #Location of offline hosts
$tempcsv = "c:\temp\temp.csv" #Location of the source file
$xlsx = "c:\temp\output.xlsx" #Desired location of output
$delimiter = "," #Specify the delimiter used in the file
#Temp export our csv - to be converted to xlsx
$exportObj | Export-Csv -Path $tempcsv -NoTypeInformation
$offlineObj | Export-Csv -Path $offlinecsv -NoTypeInformation
### Create a new Excel Workbook with one empty sheet
$excel = New-Object -ComObject excel.application
$excel.Visible = $false
$workbook = $excel.Workbooks.Add(1)
$worksheet = $workbook.worksheets.Item(1)
# Build the QueryTables.Add command and reformat the data
$TxtConnector = ("TEXT;" + $tempcsv)
$Connector = $worksheet.QueryTables.add($TxtConnector,$worksheet.Range("A1"))
$query = $worksheet.QueryTables.item($Connector.name)
$query.TextFileOtherDelimiter = $delimiter
$query.TextFileParseType = 1
$query.TextFileColumnDataTypes = ,1 * $worksheet.Cells.Columns.Count
$query.AdjustColumnWidth = 1
# Execute & delete the import query
$query.Refresh()
$query.Delete()
# Save & close the Workbook as XLSX.
$Workbook.SaveAs($xlsx,51)
$excel.Quit()
Here is an example of how I'd make an initial pass at changing your function with the primary changes being:
Multiple calls were being made to the same class, remotely. This is really inefficient. The calls were consolidated to store the result of the first call in a single variable and then reference the variable's properties for the additional information.
Your script is setting preferences, globally, a lot. All PowerShell cmdlets allow you to set the ErrorActionPreference inline, so setting it globally back and forth is unnecessary.
By prestaging an output object ($temp) and emitting it where the code quits processing, you can see the results of partial communications failures (e.g. pingable but WinRM off, etc.).
Your chassis function shouldn't have worked. Since chassisTypes returns an array, you can't use the -in operator to check if an array exists in an array. I used some code from another SO article that shows some pretty cool PowerShell on how to make this value report the chassis values accurately.
The CIM instance of Win32_LoggedOnUser returns the 'domain' and 'name' properties directly so you don't need to string-parse the values using splits and array references.
Examples of using it in the desired states you specified in the question are at the bottom (accepting multiple computer names and accepting them from the contents of a file (not with a popup, but I hope you'll see how not using GUIs will be more helpful)).
For the amount of independent remote calls you're making, if you identify that the code runs slower than you'd like, you might think about adapting it to use Invoke-Command. This would pass all of your code to the remote machine once, process it on the remote machine, and just return the output object. In my experience, this dramatically reduces the execution time of the script (e.g. I was able to pull information from thousands of servers spanning the globe in about 15 minutes using Invoke-Command, whereas individual remote calls took 12 hours or more).
Function Generate-ComputerHwReport {
param(
[Parameter(Mandatory=$true)] [string[]] [ValidateNotNullOrEmpty()] $ComputerNames ## This lines requires the passed in value to be an array of strings
)
## Modification -- Looping through the array to check all computers passed in
foreach ($ComputerName in $ComputerNames) {
$temp = [pscustomobject] #{
TestPing = $false
TestWinRM = $false
TestWSMan = $false
Hostname = $ComputerName
LastUser = ''
LastBootTime = ''
ChassisType = ''
OS = ''
InstallDate = ''
Processor = ''
Disk = ''
Ram = ''
SerialNumber = ''
Manufacturer = ''
Model = ''
ErrorLog = ''
}
#Check computer online in network, if is not online, the hostname will be skipped but necessary add in log entry which computers is offline
if (Test-Connection $ComputerName -Count 1 -Quiet) {
$temp.TestPing = $true
} else {
$temp
continue
}
#Validade crucial service that is crucial for get remote data, and if is not possible to get this information, the hostname will be skipped
try {
$Win32_OS = Get-CimInstance win32_operatingsystem -ComputerName $ComputerName -ErrorAction Stop
$temp.TestWinRM = $true
$temp.LastBootTime = $Win32_OS.LastBootUpTime.ToString("dd/MM/yyyy hh:mm:ss")
$temp.OS = $Win32_OS.Caption
$temp.InstallDate = $Win32_OS.InstallDate.ToString("dd/MM/yyyy hh:mm:ss")
} catch {
#Hostname will be skipped
$temp
continue
}
#Validate if the WS Management service is enabled on the remote device
if ((Test-NetConnection $ComputerName -Port 5985).TcpTestSucceeded) {
$temp.TestWSMan = $true
} else {
$temp
continue
}
#Function to create the Get-WUChassisType that is performed to find out if the Chassis of the equipment is Notebook or Desktop, and it is not configured to detect virtual machine
## https://stackoverflow.com/questions/55184682/powershell-getting-chassis-types-info
$ChassisTypes = #{
Name = 'ChassisType'
Expression = {
# property is an array, so process all values
$result = foreach($value in $_.ChassisTypes)
{
switch([int]$value)
{
1 {'Other'}
2 {'Unknown'}
3 {'Desktop'}
4 {'Low Profile Desktop'}
5 {'Pizza Box'}
6 {'Mini Tower'}
7 {'Tower'}
8 {'Portable'}
9 {'Laptop'}
10 {'Notebook'}
11 {'Hand Held'}
12 {'Docking Station'}
13 {'All in One'}
14 {'Sub Notebook'}
15 {'Space-Saving'}
16 {'Lunch Box'}
17 {'Main System Chassis'}
18 {'Expansion Chassis'}
19 {'SubChassis'}
20 {'Bus Expansion Chassis'}
21 {'Peripheral Chassis'}
22 {'Storage Chassis'}
23 {'Rack Mount Chassis'}
24 {'Sealed-Case PC'}
default {"$value"}
}
}
$result
}
}
$temp.ChassisType = (Get-CimInstance -ClassName Win32_SystemEnclosure -ComputerName $ComputerName | Select-Object -Property $ChassisTypes).ChassisType
#Function to get last logged user on remote computer
try {
$t = Get-CimInstance win32_loggedonuser -ComputerName $ComputerName -ErrorAction Stop | Select Antecedent -Unique
$temp.LastUser = "{0}\{1}" -f $t.Antecedent.Domain, $t.Antecedent.Name
} catch {
$temp.ErrorLog += $_
}
#Processor
$temp.Processor = (Get-CimInstance win32_processor -ComputerName $ComputerName -filter "deviceid='CPU0'").Name
#Disk
$temp.Disk = ((Get-CimInstance Win32_LogicalDisk -ComputerName $ComputerName | Select-Object #{Name="Size"; Expression={"$([math]::round($_.Size / 1GB,2))GB"}}).Size) -join ', '
#Ram Memory
$temp.Ram = ((Get-CimInstance Win32_PhysicalMemory -ComputerName $ComputerName | Select-Object #{Name="Capacity"; Expression={"$([math]::round($_.Capacity / 1GB,2))GB"}}).Capacity) -join ', '
#Serial Number
$temp.SerialNumber = Get-CimInstance win32_bios -ComputerName $ComputerName | Select-Object -ExpandProperty SerialNumber
#Manufacturer
$temp.Manufacturer = Get-CimInstance win32_ComputerSystemProduct -ComputerName $ComputerName | Select-Object -ExpandProperty Vendor
#Model
$temp.Model = Get-CimInstance win32_ComputerSystemProduct -ComputerName $ComputerName | Select-Object -ExpandProperty Name
$temp
}
}
## Output to console
Generate-ComputerHwReport -ComputerNames localhost, pc2
## Output to console reading in the computer names from a file
Generate-ComputerHwReport -ComputerNames (gc listofcomputernames.txt)
## Output to CSV reading in the computer names from a file
Generate-ComputerHwReport -ComputerNames (gc listofcomputernames.txt) | Export-Csv -NoTypeInformation ComputerHwReport.csv
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
I need a script that terminates all RDP sessions of an AD user.
Only the username should be given, whereupon the script terminates all RDP sessions of this user (if necessary also enforces them).
Unfortunately, the Get-RDUserSession cmdlet does not work (the ConnectionBroker cannot be found).
Unfortunately, I cannot process the result of the CMD command qwinsta in PowerShell.
Any ideas or tips?
Thank you.
You can create custom objects from qwinsta's output, filter them and use rwinsta to kill the session.
Function Get-TSSessions
{
param (
[Parameter(Mandatory = $true, Position = 0 )]
[String]$ComputerName
) # End Parameter Block
qwinsta /server:$ComputerName |
ForEach-Object{
If($_ -notmatch "SESSIONNAME")
{
New-Object -TypeName PSObject -Property `
#{
"ID" = [Int]$_.SubString(41,05).Trim()
"ComputerName" = $Computer
"User" = $_.SubString(19,22).Trim()
"State" = $_.SubString(47,08).Trim()
}
}
}
} # End Function Get-TSSessions
Get-TSSessions -ComputerName <ServerName> |
Where-Object{$_.User -eq "SomeUser"} |
ForEach{ & "rwinsta /Server:$($_.ComputerName) $($_.ID)" }
Obviously, you can improve by wrapping up the rwinsta command in its own function. At the moment I only have reporting work written around this sort of thing, so in the spirit of answering the question without writing the whole thing, this should get you through.
Also, I believe there are a number of scripts and functions available for this on the PowerShell Gallery. In fact, I think there were functions Get/Stop-TerminalSession in the PowerShell Community Extensions, which you can install as a module.
param
(
[Parameter(Mandatory = $false,
HelpMessage = 'Specifies the user name (SamAccountName).',
DontShow = $false)]
[SupportsWildcards()]
[ValidateNotNullOrEmpty()]
[ValidateScript({
Import-Module -Name 'ActiveDirectory' -Force
if (Get-ADUser -Filter "sAMAccountName -eq '$_'") {
return $true
} else {
return $false
}
})]
[string]$Username = $env:USERNAME
)
$ErrorActionPreference = 'SilentlyContinue'
Import-Module -Name 'ActiveDirectory' -Force
foreach ($system in (Get-ADComputer -Filter ("Name -ne '$env:COMPUTERNAME' -and OperatingSystem -like 'Windows Server*'"))) {
[string]$system = $system.Name
$session = ((quser /server:$system | Where-Object {
$_ -match $Username
}) -split ' +')[3]
if ($session) {
logoff $session /server:$system
}
}
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.