Powershell querying users in Active Directory - powershell

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

Related

Array in a foreach loop

What am I doing wrong here?
The mailbox has an active an inactive mailbox so it will return two mailboxes.
However, when trying to capture the output, I am only getting the last account in the array
Note, this is a simplified version of a larger script, but kept it simple for this example.
$guid = import-csv "c:\temp\Mailboxes.csv"
$DAta = New-Object psobject
$Data | Add-Member -MemberType NoteProperty -Name alias -Value $null
$Data | Add-Member -MemberType NoteProperty -Name guid -Value $null
$mbxcol = #()
#$data = $null
foreach ($G in $Guid){
$mbx = Get-mailbox $g.alias -IncludeInactiveMailbox
$data.alias = $mbx.alias
$data.guid = $mbx.guid
$MBXCol += $Data
}
$mbxcol
As explained in comments, every array element is a reference of the same object ($Data), a simple way to demonstrate using Object.ReferenceEquals Mehod with this example:
foreach ($item in 0..10) {
$data.Alias = 'mailbox{0}' -f $item
$data.Guid = [guid]::NewGuid()
$mbxcol += $data
}
[object]::ReferenceEquals($data, $mbxcol[0]) # -> True
As for how to simplify and make your code more efficient, do not add elements (+=) to a fixed collection (#( )):
$result = (Import-Csv "c:\temp\Mailboxes.csv").Alias |
Get-Mailbox -IncludeInactiveMailbox |
Select-Object Alias, Guid
A much more simple example of your code is:
$guid = ("A","B")
$Data = New-Object psobject
$Data | Add-Member -MemberType NoteProperty -Name alias -Value $null
$mbxcol = #()
foreach ($G in $Guid){
$mbx = $g
$data.alias = $mbx
$MBXCol += $Data
}
$mbxcol
As #Santiago mentioned in his comment, $Data is a reference to an object, so each time you update it, you overwrite it, even if it is in an array. To fix this, instantiate the object each loop as follows:
$guid = ("A","B")
$mbxcol = #()
foreach ($G in $Guid){
$Data = New-Object psobject
$Data | Add-Member -MemberType NoteProperty -Name alias -Value $null
$mbx = $g
$data.alias = $mbx
$MBXCol += $Data
}
$mbxcol

Not able to add data to csv file powershell

I am trying to add data to an csv file.
I am creating the csv with header first and then trying to add the rows. but it is returning blank csv file
$props=[ordered]#{
ServerName=''
SystemFolderPath=''
IdenityReference=''
FileSystemRights=''
}
New-Object PsObject -Property $props |
Export-Csv "C:\status_report.csv" -NoTypeInformation
$serverlist = Get-Content -Path "C:\ServerList.txt"
foreach($server in $serverlist)
{
$paths_list = $env:Path -Split ';'
Foreach ($sys_Path in $paths_list)
{
$Permissions = Get-Acl -Path $sys_Path
$Users_Permissions = $Permissions.Access | Where-Object {$_.IdentityReference}
#$Users_Permission
Foreach ($user in $Users_Permissions)
{
$IdenityReference = $user.IdentityReference.Value
$FileSystemRights = $user.FileSystemRights
$NewLine = "{0},{1},{2},{3}" -f $server,$sys_Path,$IdenityReference,$FileSystemRights
$NewLine | Export-Csv -Path "C:\status_report.csv" -Append -NoTypeInformation -Force
}
}
}
Please let me know what I am doing wrong here
The main reason why you're seeing this is because Export-Csv expects an object or object[] through the pipeline and you're passing a formatted string instead. This is specified on MS Docs:
Do not format objects before sending them to the Export-CSV cmdlet. If Export-CSV receives formatted objects the CSV file contains the format properties rather than the object properties.
PS /> 'server01,C:\Windows,Computer\User,FullControl' | ConvertTo-Csv
"Length"
"45"
Instead of appending to a CSV which is quite inefficient, unless there is a specific need for this, what you will want to do is collect the results first and then export them.
I'm not too sure why | Where-Object { $_.IdentityReference } is needed, I left it there but I don't think it's needed.
Regarding $serverlist, if you will run this on remote hosts you would be better of using Invoke-Command since it allows parallel invocations. The outer loop wouldn't be needed in that case:
$serverlist = Get-Content -Path "C:\ServerList.txt"
# Collect results here
$result = Invoke-Command -ComputerName $serverlist -ScriptBlock {
$paths_list = $env:Path -Split [System.IO.Path]::PathSeparator
foreach($sys_Path in $paths_list)
{
$Permissions = (Get-Acl -Path $sys_Path).Access
foreach($acl in $Permissions)
{
if(-not $acl.IdentityReference)
{
continue
}
[pscustomobject]#{
ComputerName = $env:ComputerName
SystemFolderPath = $sys_Path
IdenityReference = $acl.IdentityReference.Value
FileSystemRights = $acl.FileSystemRights
}
}
}
} -HideComputerName
$result | Export-Csv -Path "C:\status_report.csv" -NoTypeInformation
Accept Santiago above but this is what I did with what you wrote.
$props = [ordered]#{
ServerName = ''
SystemFolderPath = ''
IdenityReference = ''
FileSystemRights = ''
}
New-Object PsObject -Property $props |
Export-Csv "C:\status_report.csv" -NoTypeInformation
$serverlist = Get-Content -Path "C:\ServerList.txt"
$result = $serverlist | ForEach-Object {
foreach ($server in $_) {
$paths_list = $null
$paths_list = $env:Path -Split ';'
Foreach ($sys_Path in $paths_list) {
$Permissions = Get-Acl -Path $sys_Path
$Users_Permissions = $Permissions.Access | Where-Object { $_.IdentityReference }
#$Users_Permission
Foreach ($user in $Users_Permissions) {
$IdenityReference = $null
$FileSystemRights = $null
$IdenityReference = $user.IdentityReference.Value
$FileSystemRights = $user.FileSystemRights
[PSCustomObject]#{
Server = $server
Sys_Path = $sys_Path
Referecent = $IdenityReference
Rights = $FileSystemRights
}
$sys_Path = $null
}
}
}
}
$result | Export-Csv -Path "C:\status_report.csv" -NoTypeInformation
Santiago's answer is correct and contains all the required information for you to understand the issue you have here.
I just wanted to provide you with the minimum modifications to be done in your script:
Replace the $props custom object by a function (i.e CreateCustomObject)
function CreateCustomObject($val1, $val2, $val3, $val4) {
$NewObject = New-Object PSObject ;
Add-Member -InputObject $NewObject -MemberType NoteProperty -Name "ServerName" -Value $val1 ;
Add-Member -InputObject $NewObject -MemberType NoteProperty -Name "SystemFolderPath" -Value $val2 ;
Add-Member -InputObject $NewObject -MemberType NoteProperty -Name "IdenityReference" -Value $val3 ;
Add-Member -InputObject $NewObject -MemberType NoteProperty -Name "FileSystemRights" -Value $val4 ;
return $NewObject ;
}
Replace the String Variable $NewLine by an Array
$NewLine = #()
$NewLine += CreateCustomObject $server $sys_Path $IdenityReference $FileSystemRights
Write to CSV only once data is collected (move the command to the end of the script)
So the final script will look something like that:
function CreateCustomObject($val1, $val2, $val3, $val4) {
$NewObject = New-Object PSObject ;
Add-Member -InputObject $NewObject -MemberType NoteProperty -Name "ServerName" -Value $val1 ;
Add-Member -InputObject $NewObject -MemberType NoteProperty -Name "SystemFolderPath" -Value $val2 ;
Add-Member -InputObject $NewObject -MemberType NoteProperty -Name "IdenityReference" -Value $val3 ;
Add-Member -InputObject $NewObject -MemberType NoteProperty -Name "FileSystemRights" -Value $val4 ;
return $NewObject ;
}
$serverlist = Get-Content -Path "C:\Temp\ServerList.txt"
$NewLine = #()
foreach($server in $serverlist) {
$paths_list = $env:Path -Split ';'
Foreach ($sys_Path in $paths_list) {
$Permissions = Get-Acl -Path $sys_Path
$Users_Permissions = $Permissions.Access | Where-Object {$_.IdentityReference}
#$Users_Permission
Foreach ($user in $Users_Permissions) {
$IdenityReference = $user.IdentityReference.Value
$FileSystemRights = $user.FileSystemRights
$NewLine += CreateCustomObject $server $sys_Path $IdenityReference $FileSystemRights
}
}
}
$NewLine | Export-Csv -Path "C:\temp\status_report.csv" -NoTypeInformation -Force

Extracting ADUser from a list of AD groups in different domain

I have this code that extracts ADuser information from a list of AD groups, However there are some AD groups which are located on a different domain. The problem is that the code only extracts the ADuser information of the First Domain1 mentioned on the domain list, it does not go to the other domain and extract the information of the AD group that is in a different domain.
$outFile=".\Output.csv"
$report=#()
$ADGroups = Get-Content ".\ADGroups.txt"
$Domains = #("Domain1.com","Domain2.com","Domain3.com","Domain4.com","Domain5.com")
foreach ($ADGroup in $ADGroups)
{
try{
Foreach ($Domain in $Domains)
{
$ADGroupMem = Get-ADGroupMember -Identity $ADGroup -server $Domain
ForEach ($Member in $ADGroupMem){
$ADUser = $Member | Get-ADUser -Properties SamAccountName,Department,Title,Manager,l,Mobile
[PSCustomObject]#{
UserID = $ADUser.SamAccountName
Group = $ADGroup
City = $ADUser.l
}
$localAdminsReport = new-object PSObject
$localAdminsReport | add-member -membertype NoteProperty -name "UserID" -Value $ADUser.SamAccountName
$localAdminsReport | add-member -membertype NoteProperty -name "Group" -Value $ADGroup
$localAdminsReport | add-member -membertype NoteProperty -name "City" -Value $ADUser.l
#Write output into .csv file
#write-host $report -ForegroundColor cyan
write-host $Domain
$report +=$localAdminsReport
$report|Export-Csv -Path $outFile -NoTypeInformation
}
}
}
Catch{}
}
When the report is extracted it has only extracted the information from the first Domain1 AD groups and leaves out other AD groups that are located in Domain2, Domain3.....
I've reworked your script. As mentioned in above comment you should use the -Append switch of the Export-Csv cmdlet. The other thing is that your catch clause is empty, so you won't recognize any errors. I would also recommend setting the $ErrorPreference to Stop otherwise cmdlets won't throw an error. Here is the reworked code:
$outFile = ".\Output.csv"
$report = #()
$ADGroups = Get-Content ".\ADGroups.txt"
$Domains = #("Domain1.com", "Domain2.com", "Domain3.com", "Domain4.com", "Domain5.com")
$ErrorActionPreference = "Stop"
foreach ($ADGroup in $ADGroups) {
try {
Foreach ($Domain in $Domains) {
$ADGroupMem = Get-ADGroupMember -Identity $ADGroup -server $Domain
ForEach ($Member in $ADGroupMem) {
$ADUser = $Member | Get-ADUser -Properties SamAccountName, Department, Title, Manager, l, Mobile
$localAdminsReport = new-object PSObject
$localAdminsReport | add-member -membertype NoteProperty -name "UserID" -Value $ADUser.SamAccountName
$localAdminsReport | add-member -membertype NoteProperty -name "Group" -Value $ADGroup
$localAdminsReport | add-member -membertype NoteProperty -name "City" -Value $ADUser.l
$report += $localAdminsReport
$report | Export-Csv -Path $outFile -NoTypeInformation -Append
}
}
}
Catch {
Write-Error "Exception occurred $_"
}
}
$ErrorActionPreference = "SilentlyContinue"
$report | ForEach-Object { Write-Host $_}
At the of the code the content $report is dumped for debugging purposes. Check if the $report has a difference to the exported CSV file.
You can also debug your code via Visual Studio Code, via command line degguer, or via the Powershell ISE.
Hope that helps.
This worked for me. Thank you Moerwald
$outFile = ".\Output.csv"
$report = #()
$ADGroups = Get-Content ".\ADGroupslist.txt"
foreach ($ADGroup in $ADGroups) {
$Domains = #("Domain1","Domain2","Domain3","Domain4","Domain5")
foreach ($Domain in $Domains)
{
try
{
$ADGroupMem = Get-ADGroupMember -Identity $ADGroup -server $Domain
ForEach ($Member in $ADGroupMem) {
$ADUser = $Member | Get-ADUser -Properties SamAccountName, l
[PSCustomObject]#{
UserID = $ADUser.SamAccountName
Group = $ADGroup
City = $ADUser.l
}
$localAdminsReport = new-object PSObject
$localAdminsReport | add-member -membertype NoteProperty -name "UserID" -Value $ADUser.SamAccountName
$localAdminsReport | add-member -membertype NoteProperty -name "Group" -Value $ADGroup
$localAdminsReport | add-member -membertype NoteProperty -name "City" -Value $ADUser.l
$report += $localAdminsReport
$report | Export-Csv -Path $outFile -NoTypeInformation -Append
write-host "$ADGroup : $Domain" -ForegroundColor cyan
}
}
catch
{
Write-Error "Exception occurred $_"
}
}
}
$ErrorActionPreference = "Continue"
$report | ForEach-Object { Write-Host $_}

Speeding powershell script with large csv file up

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"

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