Speeding powershell script with large csv file up - powershell

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"

Related

Find and Replace powershell script not functioning, not sure where else to go

I am fairly new to powershell scripting, so please bear with me. I am trying to write a script that looks for a legacy vulnerability id inside a column on a csv, and then looks inside a sub directory at some other powershell scripts, matches the legacy ID number on the scripts inside of that subdirectory, and replaces them with the new vulnerability ID from the first column on the csv. I probably did not explain that very well but I can eleborate. Below is what I currently have
**$list = Import-csv -Path .\Downloads\test.csv
Foreach($PSScript in (Get-ChildItem -Path .\Downloads\Scripts -Filter "*.ps1" -Recurse)){
$contents = Get-Content -Path $PSScript.FullName -Raw
foreach($item in (($list.Legacy -split ";").trim())){
if($contents -match $item){
$contents.Replace($item,($list.'Vuln ID'[$list.Legacy.IndexOf($item)]))
Write-Host $contents
}
}
}**
and this is the output
**$GroupID = "V-44745"
$Hostname = (Get-WmiObject Win32_ComputerSystem).Name
$Title = "Google Chrome Current Windows STIG"
if ($Hostname -eq $null){
$Hostname = $env:computername}
$Configuration = ""
$Regkey = (Get-ItemProperty Registry::"HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Google\Chrome").AllowOutdatedPlugins
if ($Regkey -eq '0'){
$Configuration = 'Completed'}
else{
$Configuration = 'Ongoing'}
$Audit = New-Object -TypeName System.Object
$Audit | Add-Member -MemberType NoteProperty -Name GroupID -Value $GroupID
$Audit | Add-Member -MemberType NoteProperty -Name Hostname -Value $Hostname
$Audit | Add-Member -MemberType NoteProperty -Name Configuration -Value $Configuration
$Audit | Add-Member -MemberType NoteProperty -Name Title -Value $Title
$Audit**
The $Group ID above is the value that I am trying to change
I do not know how to add the csv values in just text and keep the formatting,
I tried, but here is a picture CSV Value
So my goal is it should change $GroupID = "V-44711" to $GroupID = "V-221558". I think because I am splitting $item it is not finding it in the array.
I tried what TheMadTechnician suggested, and for some reason it started creating empty lines continuously at the end of the script. I changed
Write-Host | Set-Content
to
Write-Host $contents
just to display something else.
It returns with
$GroupID = "V-44711"
$Hostname = (Get-WmiObject Win32_ComputerSystem).Name
$Title = "Google Chrome Current Windows STIG"
if ($Hostname -eq $null){
$Hostname = $env:computername}
$Configuration = ""
$Regkey = (Get-ItemProperty Registry::"HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Google\Chrome").AllowOutdatedPlugins
if ($Regkey -eq '0'){
$Configuration = 'Completed'}
else{
$Configuration = 'Ongoing'}
$Audit = New-Object -TypeName System.Object
$Audit | Add-Member -MemberType NoteProperty -Name GroupID -Value $GroupID
$Audit | Add-Member -MemberType NoteProperty -Name Hostname -Value $Hostname
$Audit | Add-Member -MemberType NoteProperty -Name Configuration -Value $Configuration
$Audit | Add-Member -MemberType NoteProperty -Name Title -Value $Title
$Audit
I have also tried something like this
ForEach($Vuln in $List){
($Vuln.Legacy -split ";").trim() | ForEach-Object {if($contents -match $_){$contents -Replace $_,$Vuln.'Vuln ID' | Out-Null;Write-host $contents;Write-Host $_}}
It seems to be finding the value in the legacy column just fine. The replace seems to not be working
Shoutout to TheMadTechnician for setting me on the right path. My second attempt worked I just forgot to set $contents in the second half. If TheMadTechnician comments back, I would still very much be interested in finding out why that didn't work. this was my Solution
$list = Import-csv -Path "Downloads\test.csv"
Foreach($PSScript in (Get-ChildItem -Path "Downloads\Win10-STIG-Audit" -Filter "*.ps1" -Recurse)){
$contents = Get-Content -Path $PSScript.FullName -Raw
ForEach($Vuln in $List){
($Vuln.Legacy -split ";").trim() | ForEach-Object {if($contents -match $_ -and $_ -notmatch "null"){$contents.Replace($_,$Vuln.'Vuln ID') | Out-File $PSScript.FullName; break}}
}
}
You're right, because you're splitting the value it's not finding it in the array. But I think there's a better way to do this! My suggestion is to use RegEx to do the replace, and build your matching pattern per updated vuln. Something like this:
ForEach($Vuln in $List){
$Legacy = ($Vuln.Legacy -split ';' | ForEach-Object {[regex]::escape($_.trim())}) -join '|'
$Updated = $Contents -replace $Legacy, $Vuln.'Vuln ID'
$Updated | Set-Content $PSScript.FullName
Write-Host | Set-Content
}
What that does, is the first line in the CSV it will look at the Legacy column, split it on the semicolon, then for each item in that it will trim the string, and escape it for a RegEx search, then join all strings from the split with a pipe. That comes out looking like this:
V-44711|SV-57545
The way that works in a -replace is that it will match on either V-44711, or SV-5745, and replace it with $Vuln.'Vuln ID' (the updated value). Since we're splitting within the ForEach, and including the entire object not just one property we can reference other properties more easily.
Edit: The above is dumb, and won't work. It's seriously writing the file each time it loops through each Legacy. So even if it finds and updates the item it will just overwrite it with the old contents on the next loop iteration. Solution to that is to change $Updated to $Contents, and probably move the output to after the loop.
ForEach($Vuln in $List){
$Legacy = ($Vuln.Legacy -split ';' | ForEach-Object {[regex]::escape($_.trim())}) -join '|'
$Contents = $Contents -replace $Legacy, $Vuln.'Vuln ID'
Write-Host | Set-Content
}
$Contents | Set-Content $PSScript.FullName

How to export file properties in csv using powershell

I wanted to export a csv-File with File-Properties from some tif-Files.
With this command
Get-ChildItem -Recurse C:\tifs\ |
ForEach-Object {$_ | add-member -name "Owner" -membertype noteproperty `
-value (get-acl $_.fullname).owner -passthru} | Sort-Object fullname |
Select FullName,CreationTime,LastWriteTime,Length,Dimensions |
Export-Csv -Force -NoTypeInformation C:\Test\export.csv
I can export a csv just fine. But as soon as I want to add properties like vertical resolution it fails. I don't quite understand why.
In order to get to the "extended" file properties (like Dimension and Resolution metadata) you have to resort to using the Windows Visual Basic shell options from inside PowerShell as Steven helpfully pointed out. Here is a code sample that should give you the result:
$files = #()
$folder = (New-Object -ComObject Shell.Application).namespace("C:\tifs")
# Loop through each file in folder
foreach ($f in $folder.Items()) {
$a = 0
# Print all the available properties (for debugging purposes)
for ($a ; $a -le 266; $a++) {
if($folder.GetDetailsOf($f, $a)) {
Write-Host "Property: $($folder.GetDetailsOf($folder.items, $a))"
Write-Host "Value: $($folder.GetDetailsOf($f, $a))"
Write-Host "Index: $($a)"
}
}
# Store data in custom PowerShell object
$obj = New-Object -TypeName PSOBJECT
# Fill each property with the file metadata (by index number)
$obj | Add-Member -MemberType NoteProperty -Name FullName -Value $folder.GetDetailsOf($f, 194)
$obj | Add-Member -MemberType NoteProperty -Name CreationTime -Value $folder.GetDetailsOf($f, 4)
$obj | Add-Member -MemberType NoteProperty -Name LastWriteTime -Value $folder.GetDetailsOf($f, 5)
$obj | Add-Member -MemberType NoteProperty -Name Length -Value $folder.GetDetailsOf($f, 1)
$obj | Add-Member -MemberType NoteProperty -Name Dimensions -Value $folder.GetDetailsOf($f, 31)
# Add custom object to a collection
$files += $obj
}
# Export collection to CSV
$files | Export-Csv -Force C:\Test\export.csv -NoTypeInformation -Encoding UTF8
From what I can tell there's no obvious PowerShell/.Net approach to getting additional file meta data. However. there are some COM based approaches.
Check Scripting Guys
And they reference this
You will still have to correlate the data. I usually do that by building hash table keyed of same values, in you can index the metadata using the path property, then use the FullName property of the file info objects to reference it, so you can the properties.

Powershell querying users in Active Directory

I am having a hard time figuring out a more efficient way of querying info from AD. As it stands I import a .csv file of active users from our student information system. Then I want to create a new .csv file of active users info from AD. As such, I am querying AD on every user (approx 10k students.) I have a feeling I could somehow accomplish this with one query, but no luck. The students match on a numeric ID that is stored in the AD title field. The code does work, however it takes hours to run. Here is what I use:
$Users = Import-Csv "c:\DASLExport.csv" -Header #("a") | Select a
$usersarray = #()
ForEach ($Row in $Users) {
$userSearchString = $Row.a
$currentUser = (Get-ADUser -Filter {Title -eq $userSearchString} -Properties title, SamAccountName, extensionAttribute1)
$UserObj = New-Object PSObject
Add-Member -InputObject $UserObj -MemberType NoteProperty -Name "ID" -Value $($currentUser.title)
Add-Member -InputObject $UserObj -MemberType NoteProperty -Name "Username" -Value $($currentUser.SamAccountName)
Add-Member -InputObject $UserObj -MemberType NoteProperty -Name "Password" -Value $($currentUser.extensionAttribute1)
$usersarray += $UserObj
}
If($usersarray.count -gt 0) {$usersarray | Export-Csv -Path 'c:\users.csv' -NoTypeInformation}
I think, instead of query each user with Get-ADUser , Get all users with title at once and save it to a variable, Then query this variable instead.
Also, Regular Arrays are in fixed size, which mean that each time you insert new element you actually create new array and copy all the data into it, and you repeat it again and again, which take much time. so switch to ArrayList which is intend to grow, it will be much faster.
Check it yourself:
$ArrayList = New-Object System.Collections.ArrayList
$RegularArray = #()
Measure-Command { 1..10000 | % {[void]$ArrayList.Add($_)} }
Measure-Command { 1..10000 | % {$RegularArray += $_ } }
So For example try this:
$Users = Import-Csv "c:\DASLExport.csv" -Header #("a") | Select a
$ADUsers = Get-ADUser -Filter {Title -ne "$null"} -Properties title, SamAccountName, extensionAttribute1
$Usersarray = New-Object System.Collections.ArrayList
ForEach ($Row in $Users) {
$userSearchString = $Row.a
$currentUser = $ADUsers | ? {$_.Title -eq $userSearchString}
if (!$currentUser) {continue}
$UserObj = New-Object PSObject
Add-Member -InputObject $UserObj -MemberType NoteProperty -Name "ID" -Value $($currentUser.title)
Add-Member -InputObject $UserObj -MemberType NoteProperty -Name "Username" -Value $($currentUser.SamAccountName)
Add-Member -InputObject $UserObj -MemberType NoteProperty -Name "Password" -Value $($currentUser.extensionAttribute1)
[void]$usersarray.Add($UserObj)
}
If($usersarray.count -gt 0) {$usersarray | Export-Csv -Path 'c:\users.csv' -NoTypeInformation}
While #Avshalom's answer is useful, it can be improved:
[CmdletBinding()]
param
(
[Parameter(Position = 0)]
[ValidateScript({Test-Path -Path $PSItem -PathType Leaf})]
[string]
$Path = 'C:\DASLExport.csv',
[Parameter(Position = 1)]
[ValidateScript({Test-Path -Path $PSItem -PathType Leaf -IsValid})]
[string]
$Destination = 'C:\users.csv'
)
$csv = Import-Csv -Path $Path -Header a
$users = #(Get-ADUser -Filter 'Title -ne "$null"' -Properties Title, SamAccountName, extensionAttribute1)
$collection = foreach ($row in $csv)
{
$title = $row.a
$user = $users.Where{$PSItem.Title -eq $title}
if (-not $user)
{
Write-Warning -Message "User $title not found."
continue
}
[pscustomobject]#{
ID = $user.Title
Username = $user.SamAccountName
Password = $user.extensionAttribute1
}
}
$collection | Export-Csv -Path $Destination -NoTypeInformation
You can assign the output of the foreach loop to a variable directly, avoiding the need to manage a list object (although if you do opt for a list, you should use System.Collections.Generic.List<Type> since ArrayList is deprecated). Additionally, you don't need to use a Select-Object statement since your csv was already loaded and it just processes it twice in that scenario. The biggest speed improvement is not querying AD thousands of times, keeping it in a single object, but MOSTLY by not using [array]/#().
Speed comparisons:
$L = 1..100000
Measure-Command {$col = foreach ($i in $L) { $i }}
~70ms
Measure-Command {$col = [System.Collections.Generic.List[int]]::new(); foreach ($i in $L) { $col.Add($i) }}
~110ms
Measure-Command {$col = #(); foreach ($i in $L) { $col += $i }}
~46 SECONDS

Add-Member inside an outside foreach loop not exporting iterations to CSV

Input:
Servers: [1,2,3]
Services: [a,b,c]
The Server 1 has Service "a"
The Server 2 has Service "a", "b"
The Server 3 has Service "b", "c"
Expected output in CSV (gave correct output on console):
Server_Name ServerAvailability ServiceName[0] ServiceStatus[0] ServiceName[1] ServiceStatus[1]
1 Up a Running
2 Up a Running b Running
3 Up b Running c Running
Actual output in CSV:
Server_Name ServerAvailability ServiceName[0] ServiceStatus[0] ServiceName[1] ServiceStatus[1]
1 Up a Running
2 Up a Running
3 Up b Running
foreach ($s in $servers) {
foreach ($srv in $services) {
$Asrv = Get-Service -Name $srv -ComputerName $s
if ($Asrv -ne $null) {
$HashSrvs.Add($Asrv.Name, $Asrv.Status)
}
}
$infoObject = #()
$infoObject = New-Object PSObject
$infoObject | Add-Member -MemberType NoteProperty -Name "Server name" -Value $s
$infoObject | Add-Member -MemberType NoteProperty -Name "ServerAvailability" -Value $ConStatus
$i=0
foreach ($key in $HashSrvs.GetEnumerator()) {
$infoObject | Add-Member -MemberType NoteProperty -Name "ServiceName[$i]" -Value $key.Key -Force
$infoObject | Add-Member -MemberType NoteProperty -Name "ServiceStatus[$i]" -Value $key.Value -Force
$i++
}
$infoColl += $infoObject
}
$infoColl | Export-Csv -NoTypeInformation -Path .\Server_Inventory_$((Get-Date).ToString('MM-dd-yyyy')).csv -Encoding UTF8
The output is correct. No error shown. The error occurs at the CSV file. The highlighted foreach loop is not iterated. It stops at Service Name[0] and Service Status[0]. Please help me with this.
You're misunderstanding how Export-Csv works. The cmdlet takes a list of objects as input and determines the columns for the CSV from the first object in that list. For all subsequent objects missing properties are filled with null values and additional properties are omitted.
To get the result you want you'd need to know the maximum number of services and add that number of properties to each object. Alternatively you could create the CSV manually by building a list of comma-separated strings and writing that via Set-Content or Out-File. Both is doable, of course, but a much simpler, much more straightforward approach would be putting the list of services into a single column with a different delimiter:
$infoColl = foreach ($s in $servers) {
$svc = Get-Service -Name $services -Computer $s -ErrorAction SilentlyContinue |
ForEach-Object { '{0}={1}' -f ($_.Name, $_.Status) }
New-Object PSObject -Property #{
'Server name' = $s
'ServerAvailability' = $ConStatus
'Services' = $svc -join '|'
}
}

Powershell script to see currently logged in users (domain and machine) + status (active, idle, away)

I am searching for a simple command to see logged on users on server.
I know this one :
Get-WmiObject -Class win32_computersystem
but this will not provide me the info I need.
It returns :
domain
Manufactureer
Model
Name (Machine name)
PrimaryOwnerName
TotalPhysicalMemory
I run Powershell 3.0 on a Windows 2012 server.
Also
Get-WmiObject Win32_LoggedOnUser -ComputerName $Computer | Select Antecedent -Unique
gives me not the exact answers I need.
I would love to see as well the idle time, or if they are active or away.
In search of this same solution, I found what I needed under a different question in stackoverflow:
Powershell-log-off-remote-session. The below one line will return a list of logged on users.
query user /server:$SERVER
Since we're in the PowerShell area, it's extra useful if we can return a proper PowerShell object ...
I personally like this method of parsing, for the terseness:
((quser) -replace '^>', '') -replace '\s{2,}', ',' | ConvertFrom-Csv
Note: this doesn't account for disconnected ("disc") users, but works well if you just want to get a quick list of users and don't care about the rest of the information. I just wanted a list and didn't care if they were currently disconnected.
If you do care about the rest of the data it's just a little more complex:
(((quser) -replace '^>', '') -replace '\s{2,}', ',').Trim() | ForEach-Object {
if ($_.Split(',').Count -eq 5) {
Write-Output ($_ -replace '(^[^,]+)', '$1,')
} else {
Write-Output $_
}
} | ConvertFrom-Csv
I take it a step farther and give you a very clean object on my blog.
I ended up making this into a module.
There's no "simple command" to do that. You can write a function, or take your choice of several that are available online in various code repositories. I use this:
function get-loggedonuser ($computername){
#mjolinor 3/17/10
$regexa = '.+Domain="(.+)",Name="(.+)"$'
$regexd = '.+LogonId="(\d+)"$'
$logontype = #{
"0"="Local System"
"2"="Interactive" #(Local logon)
"3"="Network" # (Remote logon)
"4"="Batch" # (Scheduled task)
"5"="Service" # (Service account logon)
"7"="Unlock" #(Screen saver)
"8"="NetworkCleartext" # (Cleartext network logon)
"9"="NewCredentials" #(RunAs using alternate credentials)
"10"="RemoteInteractive" #(RDP\TS\RemoteAssistance)
"11"="CachedInteractive" #(Local w\cached credentials)
}
$logon_sessions = #(gwmi win32_logonsession -ComputerName $computername)
$logon_users = #(gwmi win32_loggedonuser -ComputerName $computername)
$session_user = #{}
$logon_users |% {
$_.antecedent -match $regexa > $nul
$username = $matches[1] + "\" + $matches[2]
$_.dependent -match $regexd > $nul
$session = $matches[1]
$session_user[$session] += $username
}
$logon_sessions |%{
$starttime = [management.managementdatetimeconverter]::todatetime($_.starttime)
$loggedonuser = New-Object -TypeName psobject
$loggedonuser | Add-Member -MemberType NoteProperty -Name "Session" -Value $_.logonid
$loggedonuser | Add-Member -MemberType NoteProperty -Name "User" -Value $session_user[$_.logonid]
$loggedonuser | Add-Member -MemberType NoteProperty -Name "Type" -Value $logontype[$_.logontype.tostring()]
$loggedonuser | Add-Member -MemberType NoteProperty -Name "Auth" -Value $_.authenticationpackage
$loggedonuser | Add-Member -MemberType NoteProperty -Name "StartTime" -Value $starttime
$loggedonuser
}
}
Maybe you can do something with
get-process -includeusername
If you want to find interactively logged on users, I found a great tip here :https://p0w3rsh3ll.wordpress.com/2012/02/03/get-logged-on-users/ (Win32_ComputerSystem did not help me)
$explorerprocesses = #(Get-WmiObject -Query "Select * FROM Win32_Process WHERE Name='explorer.exe'" -ErrorAction SilentlyContinue)
If ($explorerprocesses.Count -eq 0)
{
"No explorer process found / Nobody interactively logged on"
}
Else
{
ForEach ($i in $explorerprocesses)
{
$Username = $i.GetOwner().User
$Domain = $i.GetOwner().Domain
Write-Host "$Domain\$Username logged on since: $($i.ConvertToDateTime($i.CreationDate))"
}
}
Here is my Approach based on DarKalimHero's Suggestion by selecting only on Explorer.exe processes
Function Get-RdpSessions
{
param(
[string]$computername
)
$processinfo = Get-WmiObject -Query "select * from win32_process where name='explorer.exe'" -ComputerName $computername
$processinfo | ForEach-Object { $_.GetOwner().User } | Sort-Object -Unique | ForEach-Object { New-Object psobject -Property #{Computer=$computername;LoggedOn=$_} } | Select-Object Computer,LoggedOn
}
Another solution, also based on query user, but can handle variations in culture (as far as I can tell) and produces strongly-typed results (i.e. TimeSpan and DateTime values):
# Invoke "query user", it produces an output similar to this, but might be culture-dependant!
#
# USERNAME SESSIONNAME ID STATE IDLE TIME LOGON TIME
# >jantje rdp-tcp#55 2 Active . 3/29/2021 4:24 PM
# pietje 4 Disc 49+01:01 4/14/2021 9:26 AM
$result = (&query 'user' | Out-String -Stream)
# Take the header text and insert a '|' before the start of every HEADER - although defined as inserting a bar after
# every 2 or more spaces, or after the space at the start.
$fencedHeader = $result[0] -replace '(^\s|\s{2,})', '$1|'
# Now get the positions of all bars.
$fenceIndexes = ($fencedHeader | Select-String '\|' -AllMatches).Matches.Index
$timeSpanFormats = [string[]]#("d\+hh\:mm", "h\:mm", "m")
$entries = foreach($line in $result | Select-Object -Skip 1)
{
# Insert bars on the same positions, and then split the line into separate parts using these bars.
$fenceIndexes | ForEach-Object { $line = $line.Insert($_, "|") }
$parts = $line -split '\|' | ForEach-Object { $_.Trim() }
# Parse each part as a strongly typed value, using the UI Culture if needed.
[PSCustomObject] #{
IsCurrent = ($parts[0] -eq '>');
Username = $parts[1];
SessionName = $parts[2];
Id = [int]($parts[3]);
State = $parts[4];
IdleTime = $(if($parts[5] -ne '.') { [TimeSpan]::ParseExact($parts[5], $timeSpanFormats, [CultureInfo]::CurrentUICulture) } else { [TimeSpan]::Zero });
LogonTime = [DateTime]::ParseExact($parts[6], "g", [CultureInfo]::CurrentUICulture);
}
}
# Yields the following result:
#
# IsCurrent Username SessionName Id State IdleTime LogonTime
# --------- -------- ----------- -- ----- -------- ---------
# True jantje rdp-tcp#32 2 Active 00:00:00 3/29/2021 4:24:00 PM
# False pietje 4 Disc 48.11:06:00 4/14/2021 9:26:00 AM
$entries | Format-Table -AutoSize
Team!
I have pretty nice solution to get local session as [PSObject].
Function Get-LocalSession {
<#
.DESCRIPTION
Get local session. Pasre output of command - 'query session'.
#>
[OutputType([PSObject[]])]
[CmdletBinding()]
Param(
)
try {
#region functions
#endregion
$Result = #()
$Output = . query.exe 'session' | select-object -skip 1
#use regex to parse
$pattern = '^(?<This>.)(?<SessionName>[^\s]*)\s*(?<UserName>[a-z]\w*)?\s*(?<Id>[0-9]*)\s*(?<State>\w*)\s*((?<Type>\w*)\s*)?(?<Device>\w*)?'
foreach ( $line in $output ){
$match = [regex]::Matches( $line, $pattern )
if ( $match ){
$PSO = [PSCustomObject]#{
This = $match[0].groups['This'].Value
SessionName = $match[0].groups['SessionName'].Value
UserName = $match[0].groups['UserName'].Value
Id = $match[0].groups['Id'].Value
State = $match[0].groups['State'].Value
Type = $match[0].groups['Type'].Value
Device = $match[0].groups['Device'].Value
}
$Result += $PSO
}
Else {
write-host "Unable to process line [$line] in function [Get-LocalSession]!"
}
}
}
catch {
#Get-ErrorReporting -Trap $PSItem
write-host $PSItem
}
return $Result
}
#Run it
$SessionObject = Get-LocalSession
$SessionObject | format-table -autosize -property *
I have edited mjolinor script to remove duplicate records, and dummy account names such as system, network services,...etc
If you want to get all users
function get-loggedonuser ($computername){
$regexa = '.+Domain="(.+)",Name="(.+)"$'
$regexd = '.+LogonId="(\d+)"$'
$logontype = #{
"0"="Local System"
"2"="Interactive" #(Local logon)
"3"="Network" # (Remote logon)
"4"="Batch" # (Scheduled task)
"5"="Service" # (Service account logon)
"7"="Unlock" #(Screen saver)
"8"="NetworkCleartext" # (Cleartext network logon)
"9"="NewCredentials" #(RunAs using alternate credentials)
"10"="RemoteInteractive" #(RDP\TS\RemoteAssistance)
"11"="CachedInteractive" #(Local w\cached credentials)
}
$logon_sessions = #(gwmi win32_logonsession -ComputerName $computername)
$logon_users = #(gwmi win32_loggedonuser -ComputerName $computername)
$session_user = #{}
$logon_users |% {
$_.antecedent -match $regexa > $nul
$username = $matches[1] + "\" + $matches[2]
$_.dependent -match $regexd > $nul
$session = $matches[1]
$session_user[$session] += $username
}
$logon_sessions |%{
$starttime = [management.managementdatetimeconverter]::todatetime($_.starttime)
if ($session_user[$_.logonid] -notin $loggedonuser.user -and $session_user[$_.logonid] -notlike "*$*"){
$loggedonuser = New-Object -TypeName psobject
$loggedonuser | Add-Member -MemberType NoteProperty -Name "Session" -Value $_.logonid
$loggedonuser | Add-Member -MemberType NoteProperty -Name "User" -Value $session_user[$_.logonid]
$loggedonuser | Add-Member -MemberType NoteProperty -Name "Type" -Value $logontype[$_.logontype.tostring()]
$loggedonuser | Add-Member -MemberType NoteProperty -Name "Auth" -Value $_.authenticationpackage
$loggedonuser | Add-Member -MemberType NoteProperty -Name "StartTime" -Value $starttime
$loggedonuser
}
}
}
if you want to have only domain users
function get-loggedonuser ($computername){
$HST= hostname
$regexa = '.+Domain="(.+)",Name="(.+)"$'
$regexd = '.+LogonId="(\d+)"$'
$logontype = #{
"0"="Local System"
"2"="Interactive" #(Local logon)
"3"="Network" # (Remote logon)
"4"="Batch" # (Scheduled task)
"5"="Service" # (Service account logon)
"7"="Unlock" #(Screen saver)
"8"="NetworkCleartext" # (Cleartext network logon)
"9"="NewCredentials" #(RunAs using alternate credentials)
"10"="RemoteInteractive" #(RDP\TS\RemoteAssistance)
"11"="CachedInteractive" #(Local w\cached credentials)
}
$logon_sessions = #(Get-WmiObject win32_logonsession -ComputerName $computername)
$logon_users = #(Get-WmiObject win32_loggedonuser -ComputerName $computername)
$session_user = #{}
$logon_users |ForEach-Object {
$_.antecedent -match $regexa > $nul
$username = $matches[1] + "\" + $matches[2]
$_.dependent -match $regexd > $nul
$session = $matches[1]
$session_user[$session] += $username
}
$logon_sessions |ForEach-Object{
if ($session_user[$_.logonid] -notin $loggedonuser.user -and $session_user[$_.logonid] -notlike "*$*" -and $session_user[$_.logonid] -notlike "*$HST*"){
$loggedonuser = New-Object -TypeName psobject
$loggedonuser | Add-Member -MemberType NoteProperty -Name "Session" -Value $_.logonid
$loggedonuser | Add-Member -MemberType NoteProperty -Name "User" -Value $session_user[$_.logonid]
$loggedonuser | Add-Member -MemberType NoteProperty -Name "Type" -Value $logontype[$_.logontype.tostring()]
$loggedonuser | Add-Member -MemberType NoteProperty -Name "Auth" -Value $_.authenticationpackage
$loggedonuser | Add-Member -MemberType NoteProperty -Name "StartTime" -Value $starttime
$loggedonuser
}
}
}
This is what I just figured out and works out great!
Get-Process -IncludeUserName | Select-Object -Unique | Where-Object {$_.UserName -notlike 'NT AUTHORITY\SYSTEM' -and $_.UserName -notlike 'NT AUTHORITY\NETWORK SERVICE' -and $_.UserName -notlike 'NT AUTHORITY\LOCAL SERVICE'} | Format-Table -Wrap -AutoSize