How do I log off all currently logged on users? - powershell

I would like to create a GPO that includes a scheduled task, that should be run once every day. The scheduled task should get a list of all users currently logged on a specific workstation and log them off (just lock Windows would be prefered, but a regular log off seems easier to make)
First I tried to execute the following with the scheduled task:
"shutdown.exe /f /l" to log off users. But the command was being executed by the SYSTEM user, so all regular users was not being logged off.
2nd idea was then to create a Powershell Script and place on each workstation. The script should be able to extract all currently logged on users on the workstation and log off / lock screen for all users.
$Sessions = quser
# Parse the session IDs from the output
$SessionIds = ($Sessions -split ' +')[3]
$SessionIds
# Loop through each session ID and pass each to the logoff command
ForEach-Object ($SessionId in $SessionIds){
logoff $SessionId
}

The expression ($Sessions -split ' +')[3] gives you the 4th element out of all elements produced by the split. What you actually want is the 4th element from each split line. Also, you should index from the end of the line, because otherwise you'd miss disconnected sessions, as those don't have a session name:
PS C:\> quser
USERNAME SESSIONNAME ID STATE IDLE TIME LOGON TIME
foo 7 Disc 1:23 23.10.2019 09:12
>bar rdp-tcp#114 12 Active . 23.10.2019 10:12
baz rdp-tcp#112 13 Active . 23.10.2019 10:13
Something like this should work:
quser | Select-Object -Skip 1 | ForEach-Object {
$id = ($_ -split ' +')[-5]
logoff $id
}
You could also use qwinsta instead of quser, but then you should filter out the listener as well as the "services" (and maybe the "console") session before terminating sessions.

Neither of the above solutions worked for me for all cases because of the variation in blank fields and time formatting. The following worked better.
$first = 1
quser | ForEach-Object {
if ($first -eq 1) {
$userPos = $_.IndexOf("USERNAME")
$sessionPos = $_.IndexOf("SESSIONNAME")
$idPos = $_.IndexOf("ID")
$statePos = $_.IndexOf("STATE")
$first = 0
}
else {
$user = $_.substring($userPos,$sessionPos-$userPos).Trim()
$session = $_.substring($sessionPos,$idPos-$sessionPos).Trim()
$id = $_.substring($idPos,$statePos-$idPos).Trim()
Write-Output "Logging off user:$user session:$session id:$id"
logoff $id
}
}

Might as well turn this into a parser.
# q.ps1
$first = 1
quser 2>$null | ForEach-Object {
if ($first -eq 1) {
$userPos = $_.IndexOf("USERNAME")
$sessionPos = $_.IndexOf("SESSIONNAME")
$idPos = $_.IndexOf("ID") - 2 # ID is right justified
$statePos = $_.IndexOf("STATE")
$idlePos = $_.IndexOf("IDLE TIME")
$logonPos = $_.IndexOf("LOGON TIME")
$first = 0
}
else {
$user = $_.substring($userPos,$sessionPos-$userPos).Trim()
$session = $_.substring($sessionPos,$idPos-$sessionPos).Trim()
$id = [int]$_.substring($idPos,$statePos-$idPos).Trim()
$state = $_.substring($statePos,$idlePos-$statePos).Trim()
$idle = $_.substring($idlePos,$logonPos-$idlePos).Trim()
$logon = [datetime]$_.substring($logonPos,$_.length-$logonPos).Trim()
[pscustomobject]#{User = $user; Session = $session; ID = $id;
State = $state; Idle = $idle; Logon = $logon}
}
}
.\q | ft
User Session ID State Idle Logon
---- ------- -- ----- ---- -----
admin console 111 Active none 2/6/2022 11:21:00 PM
.\q | % { logoff $_.id }

I had to change the -5 to -6 based on #AnsgarWiecher's answer for Windows 10.
quser | Select-Object -Skip 1 | ForEach-Object {
$id = ($_ -split ' +')[-6]
logoff $id
}
Thank you Ansgar.

Related

Turn PowerShell Script into Process while Saving Resources

I apologize if this has been answered before, I have not been able to find an adequate answer to my needs here.
I am new to PowerShell. I have picked it up because I have a task I wish to accomplish in Windows Server 2016. I want to trigger a PowerShell script after each security event. It will check the event IDs and if it is the one I am seeking, it will pull certain pieces of data from the event log and add it to a list. I had intended to use Task Scheduler to accomplish this, but I ran into difficulty accounting for multiple events of the same kind happening at the same time. Since I could not easily differentiate between those logs to ensure I captured all of them, I went in a different direction.
# event name
$Name = 'Security'
# get an instance
$Log = [System.Diagnostics.EventLog]$Name
# determine what to do when an event occurs
$Action = {
# get the original event entry that triggered the event
$Evlog = $event.SourceEventArgs.Entry
$eventID = $Evlog | Select -ExpandProperty InstanceID
# do something based on the event
if ($eventID -eq 4724 -or $eventID -eq 4723)
{
$timeWritten = $Evlog | Select -ExpandProperty TimeWritten
$targetUserName = $Evlog | Select -ExpandProperty Message
$subjectUserName = $Evlog | Select -ExpandProperty Message
$entrytype = $Evlog | Select -ExpandProperty EntryType
# pulls subject and target username from 4724 logs
if ($eventID -eq 4724)
{
$SubjectUserName1 = (($subjectUserName -split '\r?\n')[$lineNo - 8]).split()[-1]
$TargetUserName1 = ($targetUserName -split '\r?\n')[$lineNo - 2].split()[-1]
}
# pulls subject and target username from 4723 logs
if ($eventID -eq 4723)
{
$SubjectUserName1 = (($subjectUserName -split '\r?\n')[$lineNo - 5]).split()[-1]
$TargetUserName1 = ($targetUserName -split '\r?\n')[$lineNo - 11].split()[-1]
}
# searches for email connected to target username
if ($TargetUserName1 -eq "")
{
$TargetEmail1 = "No User Targeted"
}else {
try {$TargetEmail1 = Get-ADUser -Identity $TargetUserName1 -Properties * | Select *mail* | Select-Object -ExpandProperty "mail"}
catch{$TargetEmail1 = "User Not Found"}
}
# specifying items that are added to list file
$array = [pscustomobject]#{
Timewritten = $timeWritten
EventID = $eventID
SubjectUserName = $SubjectUserName1
TargetUserName = $TargetUserName1
TargetEmail = $TargetEmail1
Result = $entrytype -replace ".{5}$"
}
# adding to list file
$array | Export-Csv E:\apps\me\testScript.csv -Append -NoTypeInformation
# clear memory
Get-Event | Remove-Event
}
}
# subscribe to its "EntryWritten" event
Register-ObjectEvent -InputObject $log -EventName EntryWritten -SourceIdentifier 'NewEventHandler' -Action $Action
I want to turn the script into a process that is always running, waiting for the events I am looking for and following the rest of the script once one appears. From what I understand, this script can directly pull the event that triggers it, ensuring that each event is accounted for.
When I have the ISE open and run the script, is seems to work as I want it to, but once I close the ISE, the process stops. I want this to be looking for events from startup of the server, and continue indefinitely. In a perfect world, I do not have to download and install a program that helps me do this, but I understand sometimes you simply must. (I have heard of NSSM, is it reliable?) I also want to make sure I do not waste more resources than I must to do this job.
How should I approach this problem?

Is there a AD lockout script showing actual machine

Does anyone know or have a script which tells you the actual device locking out an AD account. I have a working script which lists all users locked out in the last 3 days which tells me the DC its locked out. Rather than having to connect to this or via event log and locate the event id, i wanted to know if there was a PS script out there which would output where. Then we can go to said device and fix.
Google has brought up a few suggestions but not the clearest and some just do what i can already get via the current script.
Thanks
This returns an array of PsObjects, where:
property TargetUserName holds the user SamAccountName that is locked out
property TargetDomainName contains the computer name where the lockout originated from
property EventDate will show the time and date the lockout occurred
Code:
# get the domain controller that has the PDC Emulator Role
$pdc = (Get-ADDomain).PDCEmulator
$splat = #{
FilterHashtable = #{LogName="Security";Id=4740}
MaxEvents = 100
ComputerName = $pdc
Credential = Get-Credential -Message "Please enter credentials for '$pdc'"
}
$lockedOut = Get-WinEvent #splat | ForEach-Object {
# convert the event to XML and grab the Event node
$eventXml = ([xml]$_.ToXml()).Event
# create an ordered hashtable object to collect all data
# add some information from the xml 'System' node first
$evt = [ordered]#{
EventDate = [DateTime]$eventXml.System.TimeCreated.SystemTime
Level = [System.Diagnostics.Tracing.EventLevel]$eventXml.System.Level
}
# next see if there are childnodes under 'EventData'
if ($eventXml.EventData.HasChildNodes) {
$eventXml.EventData.ChildNodes | ForEach-Object {
$name = if ($_.HasAttribute("Name")) { $_.Name } else { $_.LocalName }
$value = $_.'#text'
if ($evt[$name]) {
# if an item with that name already exists, make it an array and append
$evt[$name] = #($evt[$name]) + $value
}
else { $evt[$name] = $value }
}
}
# output as PsCustomObject. This ensures the $result array can be written to CSV easily
[PsCustomObject]$evt
}
# output on screen
$lockedOut | fl *
# output to csv file
$lockedOut | Export-Csv -Path 'D:\lockedout.csv' -NoTypeInformation
If you want to search for a specific user (SamAccountName) for instance, just do
$lockedOut | Where-Object { $_.TargetUserName -eq 'UserSamAccountName' }
Hope that helps

Easier way to parse 'query user' in PowerShell (or quser)

I currently have the following query in PowerShell:
query user /server:$server
Which returns output:
USERNAME SESSIONNAME ID STATE IDLE TIME LOGON TIME
svc_chthost 2 Disc 1:05 8/16/2016 12:01 PM
myusername rdp-tcp 3 Active . 8/29/2016 11:29 AM
Currently, I'm using #(query user /server:$server).Count - 1 as a value to represent the number of users logged on (it's not pretty, I know). However now I would like to obtain information such as USERNAME, ID, and LOGON TIME to use in other parts of my script.
My question is surrounding an easier way to parse the information above, or maybe a better solution to my problem all together: Counting and gathering information related to logged on users.
I've found other solutions that seem to work better, but I'm sure there's got to be a simpler way to accomplish this task:
$ComputerName | Foreach-object {
$Computer = $_
try
{
$processinfo = #(Get-WmiObject -class win32_process -ComputerName $Computer -EA "Stop")
if ($processinfo)
{
$processinfo | Foreach-Object {$_.GetOwner().User} |
Where-Object {$_ -ne "NETWORK SERVICE" -and $_ -ne "LOCAL SERVICE" -and $_ -ne "SYSTEM"} |
Sort-Object -Unique |
ForEach-Object { New-Object psobject -Property #{Computer=$Computer;LoggedOn=$_} } |
Select-Object Computer,LoggedOn
}#If
}
catch
{
}
Old question, but it seems a workable solution:
(query user) -split "\n" -replace '\s\s+', ';' | convertfrom-csv -Delimiter ';'
This chunks the output into lines, as the answer above does, but then replaces more than one white space character (\s\s+) with a semi-colon, and then converts that output from csv using the semi-colon as a delimiter.
The reason for more than one white space is that the column headers have spaces in them (idle time, logon time), so with just one space it would try to interpret that as multiple columns. From the output of the command, it looks as if they always preserve at least 2 spaces between items anyway, and the logon time column also has spaces in the field.
Awesome references in the comments, and still open to more answers for this question as it should have an easier solution!
foreach ($s in $servers) #For Each Server
{
foreach($ServerLine in #(query user /server:$s) -split "\n") #Each Server Line
{
#USERNAME SESSIONNAME ID STATE IDLE TIME LOGON TIME
$Parsed_Server = $ServerLine -split '\s+'
$Parsed_Server[1] #USERNAME
$Parsed_Server[2] #SESSIONNAME
$Parsed_Server[3] #ID
$Parsed_Server[4] #STATE
$Parsed_Server[5] #IDLE TIME
$Parsed_Server[6] #LOGON TIME
}
}
This solution solves the problem for now, kind of sloppy.
For more in-depth solutions with more functionalities, check the comments on the original question :)
Function Get-QueryUser(){
Param([switch]$Json) # ALLOWS YOU TO RETURN A JSON OBJECT
$HT = #()
$Lines = #(query user).foreach({$(($_) -replace('\s{2,}',','))}) # REPLACES ALL OCCURENCES OF 2 OR MORE SPACES IN A ROW WITH A SINGLE COMMA
$header=$($Lines[0].split(',').trim()) # EXTRACTS THE FIRST ROW FOR ITS HEADER LINE
for($i=1;$i -lt $($Lines.Count);$i++){ # NOTE $i=1 TO SKIP THE HEADER LINE
$Res = "" | Select-Object $header # CREATES AN EMPTY PSCUSTOMOBJECT WITH PRE DEFINED FIELDS
$Line = $($Lines[$i].split(',')).foreach({ $_.trim().trim('>') }) # SPLITS AND THEN TRIMS ANOMALIES
if($Line.count -eq 5) { $Line = #($Line[0],"$($null)",$Line[1],$Line[2],$Line[3],$Line[4] ) } # ACCOUNTS FOR DISCONNECTED SCENARIO
for($x=0;$x -lt $($Line.count);$x++){
$Res.$($header[$x]) = $Line[$x] # DYNAMICALLY ADDS DATA TO $Res
}
$HT += $Res # APPENDS THE LINE OF DATA AS PSCUSTOMOBJECT TO AN ARRAY
Remove-Variable Res # DESTROYS THE LINE OF DATA BY REMOVING THE VARIABLE
}
if($Json) {
$JsonObj = [pscustomobject]#{ $($env:COMPUTERNAME)=$HT } | convertto-json # CREATES ROOT ELEMENT OF COMPUTERNAME AND ADDS THE COMPLETED ARRAY
Return $JsonObj
} else {
Return $HT
}
}
Get-QueryUser
or
Get-QueryUser -Json
For gathering information.
based on https://ss64.com/nt/query-user.html
$result = &quser
$result -replace '\s{2,}', ',' | ConvertFrom-Csv
My own column based take. I'm not sure how much the ID column can extend to the left. Not sure how wide the end is. This is turning out to be tricky. Maybe this way is better: Convert fixed width txt file to CSV / set-content or out-file -append?
# q.ps1
# USERNAME SESSIONNAME ID STATE IDLE TIME LOGON TIME
# js1111 rdp-tcp#20 136 Active . 6/20/2020 4:26 PM
# jx111 175 Disc . 6/23/2020 1:26 PM
# sm1111 rdp-tcp#126 17 Active . 6/23/2020 1:13 PM
#
# di111111 rdp-tcp#64 189 Active 33 7/1/2020 9:50 AM
# kp111 rdp-tcp#45 253 Active 1:07 7/1/2020 9:43 AM
#
#0, 1-22, 23-40, 41-45, 46-53, 54-64, 65-80/82
$q = quser 2>$null | select -skip 1
$q | foreach {
$result = $_ -match '.(.{22})(.{18})(.{5})(.{8})(.{11})(.{16,18})'
[pscustomobject] #{
USERNAME = $matches[1].trim()
SESSIONNAME = $matches[2].trim()
ID = [int]$matches[3].trim()
STATE = $matches[4].trim()
IdleTime = $matches[5].trim()
LogonTime = [datetime]$matches[6].trim()
}
if (! $matches) {$_}
}
Invoke-command example. This is good if you're using Guacamole.
$c = get-credential
icm comp1,comp2,comp3 q.ps1 -cr $c | ft
USERNAME SESSIONNAME ID STATE IdleTime LogonTime PSComputerName RunspaceId
-------- ----------- -- ----- -------- --------- -------------- ----------
js1 136 Disc . 6/20/2020 4:26:00 PM comp1 a8e670cd-4f31-4fd0-8cab-8aa11ee75a73
js2 137 Disc . 6/20/2020 4:26:00 PM comp2 a8e670cd-4f31-4fd0-8cab-8aa11ee75a74
js3 138 Disc . 6/20/2020 4:26:00 PM comp3 a8e670cd-4f31-4fd0-8cab-8aa11ee75a75
Here's another version. The number in the ID column can be at least 1 column before the header. I figure out where the line ends on every line. The Sessionname ends in 3 dots if it's too long, and at least 2 spaces are between each column. The column headers always start at the same place.
ID can be 4 digits. Tricky.
USERNAME SESSIONNAME ID STATE IDLE TIME LOGON TIME
rwo rdp-sxs22010... 342 Active 48 2/8/2022 1:41 PM
ym326 rdp-sxs22062... 1012 Active 9 9/27/2022 3:42 PM
cw7 rdp-tcp#4 4 Active 11:16 9/26/2022 7:58 AM
# q2.ps1
$first = 1
quser 2>$null | ForEach-Object {
if ($first -eq 1) {
$userPos = $_.IndexOf("USERNAME")
$sessionPos = $_.IndexOf("SESSIONNAME") # max length 15
$idPos = $_.IndexOf("ID") - 4 # id is right justified
# $idPos = $_.IndexOf("SESSIONNAME") + 15
$statePos = $_.IndexOf("STATE") # max length 6
$idlePos = $_.IndexOf("IDLE TIME") - 2 # right justified too
$logonPos = $_.IndexOf("LOGON TIME")
$first = 0
}
else {
$user = $_.substring($userPos,$sessionPos-$userPos).Trim()
$session = $_.substring($sessionPos,$idPos-$sessionPos).Trim()
$id = [int]$_.substring($idPos,$statePos-$idPos).Trim()
$state = $_.substring($statePos,$idlePos-$statePos).Trim()
$idle = $_.substring($idlePos,$logonPos-$idlePos).Trim()
$logon = [datetime]$_.substring($logonPos,$_.length-$logonPos).Trim()
[pscustomobject]#{User = $user; Session = $session; ID = $id;
State = $state; Idle = $idle; Logon = $logon}
}
}
Output:
User Session ID State Idle Logon
---- ------- -- ----- ---- -----
rwo rdp-sxs22010... 342 Active 48 2/8/2022 1:41:00 PM
Edited: Looks like someone have already created a script that actually works pretty well: https://gallery.technet.microsoft.com/scriptcenter/Get-LoggedOnUser-Gathers-7cbe93ea
Cant believe after so many years there is still no native PowerShell for this.
I've touched up what Tyler Dickson has done and ensure the result comes back as PSCustomObject
$Servers = #("10.x.x.x", "10.y.y.y")
$Result = #()
foreach ($Server in $Servers) {
$Lines = #(query user /server:$s) -split "\n"
foreach($Line in $Lines) #Each Server Line
{
if ($Line -match "USERNAME\s+SESSIONNAME\s+ID\s+STATE\s+IDLE TIME\s+LOGON TIME") {
continue # If is the header then skip to next item in array
}
$Parsed_Server = $Line -split '\s+'
$Result += [PSCustomObject]#{
SERVER = $Server
USERNAME = $Parsed_Server[1]
SESSIONNAME = $Parsed_Server[2]
ID = $Parsed_Server[3]
STATE = $Parsed_Server[4]
IDLE_TIME = $Parsed_Server[5]
LOGON_TIME = $Parsed_Server[6]
}
}
}
$Result | Format-Table
Example output:
SERVER USERNAME SESSIONNAME ID STATE IDLE_TIME LOGON_TIME
------ -------- ----------- -- ----- --------- ----------
10.x.x.x user01 rdp-tcp#13 6 Active . 28/06/2020
10.x.x.x user02 rdp-tcp#35 11 Active 59 29/06/2020
10.y.y.y user03 rdp-tcp#38 12 Active . 29/06/2020
10.y.y.y user04 rdp-tcp#43 14 Active 5 29/06/2020
Unfortunately, no one that proposes solutions with replace method didn't notice that it will be a data collision if SESSIONNAME will empty (it will be when user disc)
So you will have SESSIONNAME contain ID, ID contain STATE etc.
It's not good.
So I`ve fixed it by -replace 'rdp-tcp#\d{1,3}' and propose to you solution with headers.
$Header = "UserName", "ID", "State", "Idle", "Logon", "Time"
$Result = $(quser) -replace 'rdp-tcp#\d{1,3}' -replace "^[\s>]", "" -replace "\s+", "," | ConvertFrom-Csv -Header $Header
Now you can access to any object $Result.Username, $Result.Idle
Was looking for the easy solution to the query user problem that also addresses the issue when SessionName is blank. Ended up combining bits and pieces from the above and came up with this. This isn't perfect, but it does seem to work better than most.
$q = (query user) -split "\n" -replace '\s{18}\s+', " blank "
$qasobject = $q -split "\n" -replace '\s\s+', "," | convertfrom-csv
The First pass with -split will replace any chunk of 18 or more spaces with " blank ", NOTE; there are 2 spaces before and after blank.
The second pass with -split will replace anything with 2 or more spaces with a ",", then pass that through convertfrom-csv to make it an object.
If you want a quick solution and don't need all information, you can also do this:
$a = Get-CimInstance -ClassName Win32_UserProfile -ComputerName "Server-1" | where {$_.Loaded -and $_.LocalPath.split('\')[1] -eq "Users" -and $_.Special -eq $false}
$a | ft -a #{N='Name';E={$_.LocalPath.split('\')[2]}},LastUseTime,Loaded
I Further appended the above code to properly format and also consider the Disconnected users
$HaSH = #()
foreach($ServerLine in #(query user) -split "\n") {
$Report = "" | Select-Object UserName, Session, ID, State, IdleTime, LogonTime
$Parsed_Server = $ServerLine -split '\s+'
if($Parsed_Server -like "USERNAME*") {
Continue
}
$Report.UserName = $Parsed_Server[1]
$Report.Session = $Parsed_Server[2]
$Report.ID = $Parsed_Server[3]
$Report.State = $Parsed_Server[4]
$Report.IdleTime = $Parsed_Server[5]
$Report.LogonTime = $Parsed_Server[6]+" " +$Parsed_Server[7]+" "+$Parsed_Server[8]
if($Parsed_Server[3] -eq "Disc") {
$Report.Session = "None"
$Report.ID = $Parsed_Server[2]
$Report.State = $Parsed_Server[3]
$Report.IdleTime = $Parsed_Server[4]
$Report.LogonTime = $Parsed_Server[5]+" " +$Parsed_Server[6]+" "+$Parsed_Server[7]
}
if($Parsed_Server -like ">*") {
$Parsed_Server=$Parsed_Server.Replace(">","")
$Report.UserName = $Parsed_Server[0]
$Report.Session = $Parsed_Server[1]
$Report.ID = $Parsed_Server[2]
$Report.State = $Parsed_Server[3]
$Report.IdleTime = $Parsed_Server[4]
$Report.LogonTime = $Parsed_Server[5]+" " +$Parsed_Server[6]+" "+$Parsed_Server[7]
}
$HaSH+=$Report
}
$result = (&quser) -replace '\s{2,}', ',' | ConvertFrom-Csv | Select -ExpandProperty USERNAME
$loggedinuser = $result.Trim(">")

PowerShell WMI query fails to return username in logon script

I'm trying to get the username of domain users in a PowerShell logon script. Any number of different users may log into the computers in question.
A local user account (let's call it 'syscheck') is configured on Win7/Win8 domain clients for the purpose of running a PS script (PS 2.0/3.0); the script resides locally and is launched by Task Scheduler on user logon. The script needs to obtain the username of the domain user that is logging in.
I've attempted to do this with WMI:
Get-WmiObject Win32_ComputerSystem | Select-Object -ExpandProperty UserName
but this does not return anything when the script runs.
If I try this:
$env:USERNAME
The username of the 'syscheck' local account is returned.
Is the domain username not yet available when the script is running on logon?
Perhaps there a way to do this with .NET? Other options?
***** UPDATE August 8 *****
I've tested with the solution provided (thanks Alexander!) but still can NOT retrieve the username of the logged-in user. I believe this is because, as mentioned above, this is a logon script launched by Task Scheduler. The principal for the Task that launches the script is a local account. For some reason, all methods of trying to get the domain username fail.
Here is latest attempt:
First, this is how I call the function:
$indx = 0
do {
$username = GetDomUser
if (($indx -eq 25) -or ($username.Length -ne 0)) {
Write-Output $username
Break
}
else {
Start-Sleep -Seconds 12
}
$indx++
}
while ($indx -lt 25) # 5 minutes is PLENTY of time for boot...
Now, here's the function:
Function GetDomUser {
$compname = $($env:COMPUTERNAME)
$pattern = '"MYDOMAIN",Name='
$antecedent = #(Get-WmiObject -Class Win32_LoggedOnUser -ComputerName $compname |
Where-Object { $_.Antecedent -match $pattern } | Select-Object -ExpandProperty Antecedent)
Return ([regex]::Match([string]$antecedent[0],"$pattern(.*$)").Value).Split('=')[1] -replace '"', ""
}
Of course, this works perfectly from the console once the machine has booted.
Is it possible to refresh whatever store the Win32_LoggedOnUser Class gets its data from?
Other options?
Here are previous methods I've tried - all return the username of the principal of the Task that launches the script (or an empty string, which is what D returns).
$usernameA = $([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)
$usernameB = $(whoami)
$usernameC = $($env:USERNAME)
$usernameD = $(Get-WmiObject Win32_ComputerSystem -ComputerName $compname | Select-Object -ExpandProperty UserName)
$usernameE = $([Environment]::UserName)
Here's what you could do to find out what's going on:
$iLOGON32_LOGON_INTERACTIVE = 2
$cLogonSessions = Get-WmiObject -Class "Win32_LogonSession" `
| Where-Object { $_.LogonType -eq $iLOGON32_LOGON_INTERACTIVE }
if ($cLogonSessions -ne $null) {
$cInteractiveLogons = #()
foreach ($oLogonSession in $cLogonSessions) {
$sWmiQuery = ('ASSOCIATORS OF {{Win32_LogonSession.LogonId="{0}"}} ' `
+ 'WHERE AssocClass=Win32_LoggedOnUser') -f $oLogonSession.LogonId
$cInteractiveLogons += Get-WMIObject -Query $sWmiQuery `
| Select-Object -ExpandProperty "Caption"
}
} else {
$ex = New-Object -TypeName System.NullReferenceException(`
'$cInteractiveLogons is null.')
throw $ex
}
$cInteractiveLogons | Select-Object -Unique
When $cInterativeLogons is null exception is thrown, it means that no-one is logged on interactively (yet) in which case you can wait and re-check later.
Note that this code is not reliable because LOGON32_LOGON_INTERACTIVE wasn't limited to local console logons in XP and earlier versions.
As for actual solution, I'd recommend using some kind of explicit notifications. You could for example make use of events. Subscribe for an event and then emit the event from the user's regular logon script.
The problem was not with the WMI code but rather the state of the machine it was being run on. It turns out that when users are VPNed into their machines (almost always thanks to a VPN client's automated reconnect feature), or have some third-party utility installed (e.g. certain cloud backup services), there are multiple Logons and "the" logged on user is ambiguous.
For now this is working pretty well:
Function GetDomainUser {
$compname = $($env:COMPUTERNAME)
$pattern = '"' + $($env:USERDOMAIN) + '"' + ',Name='
$antecedent = #(Get-WmiObject -Class Win32_LoggedOnUser -ComputerName $compname |
Where-Object { $_.Antecedent -match $pattern } |
Select-Object -ExpandProperty Antecedent | Select-Object -Unique)
Return $(([regex]::Match([string]$antecedent,$($pattern + '(".+")')).Value).Split('=')[1] -replace '"','')
}
But I had to write addition code to work around cases when the LoggedOnUser cannot be discovered (multiple logons exist), or when no one is logged in.

How to put a header to csv file generated by Powershell script

I got this Powershell script that queries users that have not changed their password for 24 hours. The query redirects the output to csv file. Below are the Powershell script and batch script:
Powershell script:
$root = [ADSI]''
$searcher = new-object System.DirectoryServices.DirectorySearcher($root)
$searcher.filter = "(&(objectCategory=person)(objectClass=user)(!(userAccountControl:1.2.840.113556.1.4.803:=2)))"
$searcher.sizelimit = 5000
[Void]$searcher.PropertiesToLoad.Add("cn")
[Void]$searcher.PropertiesToLoad.Add("samAccountName")
[Void]$searcher.PropertiesToLoad.Add("pwdLastSet")
$users = $searcher.findall()
$UserOU = "OU=Mountain,DC=Atlanta,DC=ga"
$PWDays = (Get-Date).AddDays(-1)
$UserCount = 0
$UserPW = 0
foreach($user in $users)
{
if ($user.path -like "*$UserOU")
{
$usercount = $UserCount
if ([datetime]::FromFileTime(($user.properties.pwdlastset)[0]) -le $PWDays)
{
$UserPW = $UserPW + 1
Write-Host $user.Properties.cn
}
}
}
Batch script:
powershell.exe d:\temp\query.ps1 > D:\temp\query.csv
My question is: How do I put change the script to put header for username in the the csv output file?
The header may simple be 'Username' not necessarily Firstname and Lastname.
Any reason why you aren't using Export-Csv? You can just pipe your objects into it and it will include headers. Something along the lines of
$users |
? { $_.Path -like "*$UserOU" } |
? { [datetime]::FromFileTime(($user.properties.pwdlastset)[0]) -le $PWDays } |
% { $_ | Add-Member -PassThru NoteProperty Username $_.Properties.cn } |
select Username |
Export-Csv D:\temp\query.csv
might work. (Hint: The pipeline is more fun than the loop :))
Not sure (never have user PS) but I guess that sticking
Write-Host "Username"
before the foreach, might do the trick