I am trying to write a script that will give me back as an object the results from qwinsta.
The code of my script is this:
function Get-QWinsta {
$queryResults = (qwinsta /Server:$ENV:COMPUTERNAME | ForEach-Object { ( ( $_.Trim() -replace "\s+",",") ) } | ConvertFrom-Csv)
$queryResults | ForEach-Object { if ( $_.SESSIONNAME -match 'da-' ) {
$_.USERNAME = $_.SESSIONNAME; $_.SESSIONNAME = $null; $_.STATE = $_.ID; $_.ID = $null
}
}
$RDPSESSIONS = $queryResults | ForEach-Object {
[PSCustomObject]#{
UserName = if ( $_.USERNAME -match '\d' ) { $_.USERNAME = $null } else { $_.USERNAME }
SessionName = if ( $_.SESSIONNAME -match 'services|console' ) { $_.SESSIONNAME = $null } else { $_.SESSIONNAME }
SessionID = $_.ID
SessionState = if ( $_.ID -match 'Disc' ) { $_.STATE = 'Disconnected' } else { $_.STATE }
}
}
return $RDPSESSIONS
}
and the output is this:
UserName SessionName SessionID SessionState
-------- ----------- --------- ------------
Disc
Conn
Admin01 Disc
Admin02 rdp-tcp#41 4 Active
rdp-tcp Listen
However the above is not a real object and what I would really want to have as output is something like this:
UserName SessionName SessionID SessionState
-------- ----------- --------- ------------
Admin01 Disc
Admin02 rdp-tcp#41 4 Active
Plus if I could something like this:
> $user1 = (Get-Qwinsta).UserName
> Write-Output $user1
> Admin01
that would be a bonus.
I have read all the similar post here and everywhere on the internet and none of them worked perfectly fine or did what I want to achieve.
You can get the qwinsta results as custom objects like this:
function Get-QWinsta
{
param ($ComputerName = "$env:COMPUTERNAME")
qwinsta /server:$ComputerName |
ForEach-Object {$_ -replace "\s{2,18}",","} |
ConvertFrom-Csv
}
> Get-QWinsta -ComputerName 'Server1'
This gives a custom object for each entry in the output:
SESSIONNAME : services
USERNAME :
ID : 0
STATE : Disc
TYPE :
DEVICE :
SESSIONNAME : console
USERNAME :
ID : 1
STATE : Conn
TYPE :
DEVICE :
SESSIONNAME : session1
USERNAME : User1
ID : 23
STATE : Active
TYPE : wdica
DEVICE :
SESSIONNAME : session2
USERNAME : User2
ID : 25
STATE : Active
TYPE : wdica
DEVICE :
Which can be manipulated like other objects. For example, shown in a table:
> Get-QWinsta -ComputerName 'Server1' | Format-Table -Autosize
SESSIONNAME USERNAME ID STATE TYPE DEVICE
----------- -------- -- ----- ---- -------
services 0 Disc
console 1 Conn
session1 User1 23 Active wdica
session2 User2 25 Active wdica
session3 User3 41 Active wdica
Or, output just specific properties:
> (Get-QWinsta -ComputerName 'Server1').UserName
User1
User2
User3
Related
I've had this idea about getting the output from 2 separate functions, that return a PSCustomObject as a list, and formatting them into one table. My problem is simple... I don't know how to do it. lol
With the various of combinations that I tried, here's whats given me some promising results:
$Var1 = [PSCustomObject]#{
UserName = $env:USERNAME
Stuff1 = 'stuff1'
} | Format-List | Out-String -Stream
$Var2 = [PSCustomObject]#{
ComputerName = $env:COMPUTERNAME
Stuff2 = 'stuff2'
} | Format-List | Out-String -Stream
[PSCustomObject]#{
TableOne = $Var1.Trim().Foreach({$_})
TableTwo = $Var2.Trim()
} | Format-Table -AutoSize
The output:
TableOne TableTwo
-------- --------
{, , UserName : Abraham, Stuff1 : stuff1...} {, , ComputerName : DESKTOP-OEREJ77, Stuff2 : stuff2...}
I say promising in the respect that it shows the actual content of $var1 and 2, whereas my other attempts didn't. I also left the .foreach() operator there to show one of the many many different tricks I tried to get this working. For a quick second I thought the Out-String cmdlet would've done the trick for me, but was unsuccessful.
Has anyone ever done something similar to this?
EDIT:
Nevermind, I figured it out.
Used a for loop to iterate through each line assigning it the the PSCustomObject one at a time. Also used the .Where() operator to remove white spaces, and compared the two arrays to find the largest number to use it as the count.
$Var1 = $([PSCustomObject]#{
UserName = $env:USERNAME
Stuff1 = 'stuff1'
} | Format-List | Out-String -Stream).Where{$_ -ne ''}
$Var2 = $([PSCustomObject]#{
ComputerName = $env:COMPUTERNAME
Stuff2 = 'stuff2'
ExtraStuff = 'More'
} | Format-List | Out-String -Stream).Where{$_ -ne ''}
$Count = ($Var1.Count, $Var2.Count | Measure-Object -Maximum).Maximum
$(for($i=0;$i -lt $Count; $i++) {
[PSCustomObject]#{
TableOne = $Var1[$i]
TableTwo = $Var2[$i]
}
}) | Format-Table -AutoSize
Output:
TableOne TableTwo
-------- --------
UserName : Abraham ComputerName : DESKTOP-OEREJ77
Stuff1 : stuff1 Stuff2 : stuff2
ExtraStuff : More
It's an interesting way to format two collections with corresponding elements.
To indeed support two collections with multiple elements, a few tweaks to your approach are required:
# First collection, containing 2 sample objects.
$coll1 =
[PSCustomObject] #{
UserName = $env:USERNAME
Stuff1 = 'stuff1'
},
[PSCustomObject] #{
UserName = $env:USERNAME + '_2'
Stuff1 = 'stuff2'
}
# Second collection; ditto.
$coll2 =
[PSCustomObject] #{
ComputerName = $env:COMPUTERNAME
Stuff2 = 'stuff2'
ExtraStuff = 'More'
},
[PSCustomObject]#{
ComputerName = $env:COMPUTERNAME + '_2'
Stuff2 = 'stuff2_2'
ExtraStuff = 'More_2'
}
# Stream the two collections in tandem, and output a Format-List
# representation of each object in a pair side by side.
& {
foreach ($i in 0..([Math]::Max($coll1.Count, $coll2.Count) - 1)) {
[PSCustomObject] #{
TableOne = ($coll1[$i] | Format-List | Out-String).Trim() + "`n"
TableTwo = ($coll2[$i] | Format-List | Out-String).Trim() + "`n"
}
}
} | Format-Table -AutoSize -Wrap
The above ensures that multiple objects are properly placed next to each other, and yields something like the following:
TableOne TableTwo
-------- --------
UserName : jdoe ComputerName : WS1
Stuff1 : stuff1 Stuff2 : stuff2
ExtraStuff : More
UserName : jdoe_2 ComputerName : WS1_2
Stuff1 : stuff2 Stuff2 : stuff2_2
ExtraStuff : More_2
I've got some Reports that I'm trying to loop through, and fetch the connection ID's (User ID's) and list those reports along with the usernames.
A report can have the following scenarios:
No DataSources
1 DataSource (1 User ID)
More than 1 DataSources (Therefore more than 1 user id's)
The following script does the job, however, for some reason, reports with only 1 datasource seem to be getting executed in the else ... No Connection ID's found! statement. That shouldn't be the case considering there is AT LEAST 1 Datasource, so they should be going through this if ($DataSourceValue.DataModelDataSource.count -gt 0) statement instead!
Below is script accompanied by current output vs expected output:
$loopCount = 1
$PBI_Reports_Array = #()
$DataSourceQueue = New-Object System.Collections.Queue
try {
$PBI_Reports_Array = $(Invoke-RestMethod -UseDefaultCredentials -uri $($webPortalURL + "/api/v2.0/PowerBIReports"))
foreach ($reportPath in $PBI_Reports_Array.value.path) {
try {
foreach ($DataSource in $(Invoke-RestMethod -UseDefaultCredentials -uri $($webPortalURL + "/api/v2.0/PowerBIReports(path='" + $reportPath + "')/DataSources")))
{
$DataSourceQueue.Enqueue($DataSource)
}
while($DataSourceQueue.count)
{
$DataSourceValue = $($DataSourceQueue.Dequeue()).value
if ([string]::IsNullOrEmpty($($DataSourceValue))) {
write-host "$loopCount | $reportPath | No DataSource connection exists for this Report!";
}
else {
if ($DataSourceValue.DataModelDataSource.count -gt 0) #if there is at least 1+ DataSources and usernames...
{
#because there is more than 1 DataSources, that means there's also more than 1 connection ID and more than 1 connection ID's
#to loop through both of those variables, foreach loop would not be suitable for more than 1 variable,
#so we use good old for loop
0..($DataSourceValue.DataModelDataSource.length-1) | ForEach-Object {
write-host "$loopCount | $reportPath | $($DataSourceValue.DataModelDataSource[$_].Username) | Retrieved!"
}
}
else
{
write-host "$loopCount | $reportPath | $($DataSourceValue.DataModelDataSource.Username) | No Connection ID's found!"
}
}
}
#$loopCount++
}
catch {
write-host "$loopCount | ERROR! $($error[0])`r`n$($error[0].InvocationInfo.PositionMessage)`r`n$($error[0].ScriptStackTrace)"
}
$loopCount++
}
}
catch {
}
Current output:
1 | /Cash Flow/CFG | SI_123456_P | No Connection ID's found!
2 | /CPUR/DQ Dashboards | gp_powerbi_cpur | No Connection ID's found!
3 | /ABC/DQ Dashboards/PreCost ABC DQ Dashboard | gp_powerbi_cpur | Retrieved!
3 | /ABC/DQ Dashboards/PreCost ABC DQ Dashboard | SI_123456_P | Retrieved!
4 | /Prototypes/ARCHIVE/dummy data | | No Connection ID's found!
Expected Output:
1 | /Cash Flow/CFG | SI_123456_P | Retrieved!
2 | /CPUR/DQ Dashboards | gp_powerbi_cpur | Retrieved!
3 | /ABC/DQ Dashboards/PreCost ABC DQ Dashboard | gp_powerbi_cpur | Retrieved!
3 | /ABC/DQ Dashboards/PreCost ABC DQ Dashboard | SI_123456_P | Retrieved!
4 | /Prototypes/ARCHIVE/dummy data | | No Connection ID's found!
Note the 4th report has no connection ID's, therefore makes sense that the status is No Connection ID's found!. but reports 1 and 2 have 1 ID, so the status should be Retrieved! not No Connection ID's found! as it's currently displaying. For the 3rd report, it appears because it has more than 1 DataSource, the conditional statement is being applied correctly...
EDIT:
$DataSourceValue.DataModelDataSource.GetType()
yields:
for reports 1 & 2:
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True False PSCustomObject System.Object
for report 3:
True True Object[] System.Array
EDIT 2:
62 | /test/BMI vs Chocolate | | Retrieved!
Type : Import
Kind : File
AuthType : Windows
SupportedAuthTypes : {Windows}
Username :
Secret :
ModelConnectionName :
62 | /test/BMI vs Chocolate | | Retrieved!
Type : Import
Kind : File
AuthType : Windows
SupportedAuthTypes : {Windows}
Username :
Secret :
ModelConnectionName :
62 | /test/BMI vs Chocolate | | Retrieved!
Type : Import
Kind : File
AuthType : Windows
SupportedAuthTypes : {Windows}
Username :
Secret :
ModelConnectionName :
63 | /VP/Complexity_PROD | gp_powerbi_vp | Retrieved!
Id : 12345-cvgfgh7-87964-e76ufhf5
Name :
Description :
Path :
Type : DataSource
Hidden : False
Size : 0
ModifiedBy : user1
ModifiedDate : 2021-02-03T12:17:26.413-05:00
CreatedBy : SI_123456_P
CreatedDate : 2021-02-03T12:13:42.523-05:00
ParentFolderId :
IsFavorite : False
ContentType :
Content :
IsEnabled : True
ConnectionString : db=maxisdb;driver={DataDirect 7.1 Greenplum Wire Protocol};em=1;host=gp.companyxyz.com;port=5432;vsc=0
DataSourceType :
IsOriginalConnectionStringExpressionBased : False
IsConnectionStringOverridden : False
CredentialRetrieval : prompt
IsReference : False
DataSourceSubType : DataModel
Roles : {}
CredentialsByUser :
CredentialsInServer :
DataModelDataSource : #{Type=Import; Kind=Odbc; AuthType=UsernamePassword; SupportedAuthTypes=System.Object[]; Username=gp_powerbi_vp; Secret=; ModelConnectionName=}
Type : Import
Kind : Odbc
AuthType : UsernamePassword
SupportedAuthTypes : {UsernamePassword, Anonymous, Windows}
Username : gp_powerbi_vp
Secret :
ModelConnectionName :
I don't mean this as an answer but could you test your script by replacing this portion of code:
$DataSourceValue = $($DataSourceQueue.Dequeue()).value
if ([string]::IsNullOrEmpty($($DataSourceValue))) {
write-host "$loopCount | $reportPath | No DataSource connection exists for this Report!";
}
else
{
if ($DataSourceValue.DataModelDataSource.count -gt 0)
{
0..($DataSourceValue.DataModelDataSource.length-1) | ForEach-Object {
write-host "$loopCount | $reportPath | $($DataSourceValue.DataModelDataSource[$_].Username) | Retrieved!"
}
}
else
{
write-host "$loopCount | $reportPath | $($DataSourceValue.DataModelDataSource.Username) | No Connection ID's found!"
}
}
For this:
$DataSourceValue = $DataSourceQueue.Dequeue().Value
if (-not $DataSourceValue)
{
write-host "$loopCount | $reportPath | No DataSource connection exists for this Report!";
}
else
{
if ($DataSourceValue.DataModelDataSource.Username)
{
foreach($i in $DataSourceValue.DataModelDataSource.Username)
{
write-host "$loopCount | $reportPath | $i | Retrieved!"
}
}
else
{
write-host "$loopCount | $reportPath | $($DataSourceValue.DataModelDataSource.Username) | No Connection ID's found!"
}
}
Edit: Complete new script here, let's see if this works.
$ErrorActionPreference = 'Stop'
$loopCount = 1
$hash = #{
UseDefaultCredentials = $true
}
try
{
$hash.Uri = "$webPortalURL/api/v2.0/PowerBIReports"
$PBI_Reports_Array = (Invoke-RestMethod #hash).Value.Path
foreach ($reportPath in $PBI_Reports_Array)
{
$hash.Uri = "$webPortalURL/api/v2.0/PowerBIReports(path='$reportPath')/DataSources"
try
{
$DataSourceQueue = (Invoke-RestMethod #hash).Value
}
catch
{
Write-Warning ("$loopCount | ERROR! Can't connect to {0}" -f $hash.uri)
continue
}
foreach ($value in $DataSourceQueue)
{
if (-not $value.DataModelDataSource)
{
write-host "$loopCount | $reportPath | No DataModelDataSource"
continue
}
foreach($i in $value.DataModelDataSource)
{
if($i.Username)
{
$i.Username | foreach-object {
write-host "$loopCount | $reportPath | $_ | Retrieved!"
}
}
else
{
write-host "$loopCount | $reportPath | DataModelDataSource Found but No Usernames"
}
}
}
$loopCount++
}
}
catch
{
Write-Warning ("ERROR! Can't connect to {0}" -f $hash.uri)
}
I am attempting to view the last 5 login events on an Enterprise machine as an Admin after a security event. I do initial investigations and am trying to find a way to quickly spit out a list of potential, 'suspects'.
I have been able to generate output that lists the logfile but under account name where you would generally see \Domain\username I only get the output, "SYSTEM" or similar.
If I had recently remoted into the machine it will pull my \Domain\Username and display it no problem.
Ideally I would like to make a script that pulls the logon events from a machine on the network with a list of who logged in recently and when.
This is what I have so far:
Get-EventLog -LogName security -InstanceId 4624 -ComputerName $_Computer -Newest 5 | Export-Csv C:\Users\username\Documents\filename
this uses the far faster Get-WinEvent cmdlet & the -FilterHashtable parameter to both speed things up a tad and to add more selectors. you may want to remove some of the filters - this was written quite some time ago for another project. [grin]
#requires -RunAsAdministrator
# there REALLY otta be a way to get this list programmatically
$LogonTypeTable = [ordered]#{
'0' = 'System'
'2' = 'Interactive'
'3' = 'Network'
'4' = 'Batch'
'5' = 'Service'
'6' = 'Proxy'
'7' = 'Unlock'
'8' = 'NetworkCleartext'
'9' = 'NewCredentials'
'10' = 'RemoteInteractive'
'11' = 'CachedInteractive'
'12' = 'CachedRemoteInteractive'
'13' = 'CachedUnlock'
}
$EventLevelTable = [ordered]#{
LogAlways = 0
Critical = 1
Error = 2
Warning = 3
Informational = 4
Verbose = 5
}
$WantedLogonTypes = #(2, 3, 10, 11)
$AgeInDays = 15
$StartDate = (Get-Date).AddDays(-$AgeInDays)
$ComputerName = $env:COMPUTERNAME
$GWE_FilterHashTable = #{
Logname = 'Security'
ID = 4624
StartTime = $StartDate
#Level = 2
}
$GWE_Params = #{
FilterHashtable = $GWE_FilterHashTable
ComputerName = $ComputerName
MaxEvents = 100
}
$RawLogonEventList = Get-WinEvent #GWE_Params
$LogonEventList = foreach ($RLEL_Item in $RawLogonEventList)
{
$LogonTypeID = $RLEL_Item.Properties[8].Value
if ($LogonTypeID -in $WantedLogonTypes)
{
[PSCustomObject]#{
LogName = $RLEL_Item.LogName
TimeCreated = $RLEL_Item.TimeCreated
UserName = $RLEL_Item.Properties[5].Value
LogonTypeID = $LogonTypeID
LogonTypeName = $LogonTypeTable[$LogonTypeID.ToString()]
}
}
}
$NewestLogonPerUser = $LogonEventList |
Sort-Object -Property UserName |
Group-Object -Property UserName |
ForEach-Object {
if ($_.Count -gt 1)
{
$_.Group[0]
}
else
{
$_.Group
}
}
$NewestLogonPerUser
current output on my system ...
LogName : Security
TimeCreated : 2019-01-24 1:50:44 PM
UserName : ANONYMOUS LOGON
LogonTypeID : 3
LogonTypeName : Network
LogName : Security
TimeCreated : 2019-01-24 1:50:50 PM
UserName : [MyUserName]
LogonTypeID : 2
LogonTypeName : Interactive
I've been fiddling with this too and also decided to use the Get-WinEvent cmdlet for this, because unfortunately, using Get-EventLog the info you want is all in the .Message item and that is a localized string..
My approach is a little different than Lee_Daily's answer as I get the info from the underlying XML like this:
#logon types: https://learn.microsoft.com/en-us/windows/desktop/api/ntsecapi/ne-ntsecapi-_security_logon_type#constants
$logonTypes = 'System','Undefined','Interactive','Network','Batch','Service','Proxy','Unlock',
'NetworkCleartext','NewCredentials','RemoteInteractive','CachedInteractive',
'CachedRemoteInteractive','CachedUnlock'
$dataItems = #{
SubjectUserSid = 0
SubjectUserName = 1
SubjectDomainName = 2
SubjectLogonId = 3
TargetUserSid = 4
TargetUserName = 5
TargetDomainName = 6
TargetLogonId = 7
LogonType = 8
LogonProcessName = 9
AuthenticationPackageName = 10
WorkstationName = 11
LogonGuid = 12
TransmittedServices = 13
LmPackageName = 14
KeyLength = 15
ProcessId = 16
ProcessName = 17
IpAddress = 18
IpPort = 19
}
$result = Get-WinEvent -FilterHashtable #{LogName="Security";Id=4624} -MaxEvents 100 | ForEach-Object {
# convert the event to XML and grab the Event node
$eventXml = ([xml]$_.ToXml()).Event
# get the 'TargetDomainName' value and check it does not start with 'NT AUTHORITY'
$domain = $eventXml.EventData.Data[$dataItems['TargetDomainName']].'#text'
if ($domain -ne 'NT AUTHORITY' ) {
[PSCustomObject]#{
Domain = $domain
UserName = $eventXml.EventData.Data[$dataItems['TargetUserName']].'#text'
UserSID = $eventXml.EventData.Data[$dataItems['TargetUserSid']].'#text'
LogonType = $logonTypes[[int]$eventXml.EventData.Data[$dataItems['LogonType']].'#text']
Date = [DateTime]$eventXml.System.TimeCreated.SystemTime
Computer = $eventXml.System.Computer
}
}
}
$result | Sort-Object Date -Descending | Group-Object -Property UserName | ForEach-Object {
if ($_.Count -gt 1) { $_.Group[0] } else { $_.Group }
} | Format-Table -AutoSize
On my machine the output looks like
Domain UserName UserSID LogonType Date Computer
------ -------- ------- --------- ---- --------
MyDomain MyUserName S-1-5-21-487608883-1237982911-748711624-1000 Interactive 27-1-2019 20:36:45 MyComputer
MyDomain SomeoneElse S-1-5-21-487608883-1237982911-748765431-1013 Interactive 27-1-2019 18:36:45 MyComputer
I have a .csv file which looks like:
employeenumber;phone;mobile;fax;userid;Email
99999991;+1324569991;+234569991;+5234569991;user01;user1#domain.com
99999992;+1234569992;+234569992;;user02;user2#domain.com
99999993;+1234569993;+234569993;;user03;user3#domain.com
99999993;+12345699933;;;user03;user3#domain.com
99999993;;;+5234569993;user03;user3#domain.com
99999994;+1234569994;;;user04;user4#domain.com
As you can see there are different employeenumbers and some lines with the same employeenumber.
Is there any way to merge the lines with the same employeenumber in powershell?
Similar Output:
employeenumber;phone;mobile;fax;userid;Email
99999991;+1324569991;+234569991;+5234569991;user01;user1#domain.com
99999992;+1234569992;+234569992;;user2;user2#domain.com
99999993;+1234569993 / +12345699933;+234569993;+5234569993;user03;user3#domain.com
99999994;+1234569994;;;user04;user4#domain.com
Thank you
I've taken a shot at it. I believe my answer is easier to read than Mjolinor's.
I group the entries from the CSV into either $singletons or $duplicates, based on using the Group-Object command. Then, I pipe through the $duplicates and merge the records found in either the phone,mobile, or fax fields, using a '/' character as you've indicated.
#$csv = get-content .\CSVNeedstoMerge.csv
$csvValues = $csv | ConvertFrom-Csv -Delimiter ';'
$duplicates = $csvValues | group-object EmployeeNumber | ? Count -gt 1
$objs = New-Object System.Collections.ArrayList
$singletons = $csvValues | group-object EmployeeNumber | ? Count -eq 1 | % {$objs.Add($_.Group)}
ForEach ($duplicate in $duplicates){
$objs.Add([pscustomobject]#{employeenumber=($duplicate.Group.employeenumber | select -Unique) -as [int];
phone=($duplicate.Group.phone | ? Length -gt 0) -join '/';
mobile=($duplicate.Group.mobile| ? Length -gt 0) -join '/';
fax=($duplicate.Group.fax | ? Length -gt 0) -join '/';
userid = $duplicate.Group.userid | select -Unique
email= $duplicate.Group.email | select -Unique })
}
$objs | Sort EmployeeNumber
I'll give that a shot:
(#'
employeenumber;phone;mobile;fax;userid;Email
99999991;+1324569991;+234569991;+5234569991;user01;user1#domain.com
99999992;+1234569992;+234569992;;user02;user2#domain.com
99999993;+1234569993;+234569993;;user03;user3#domain.com
99999993;+12345699933;;;user03;user3#domain.com
99999993;;;+5234569993;user03;user3#domain.com
99999994;+1234569994;;;user04;user4#domain.com
'#).split("`n") |
foreach {$_.trim()} | sc test.csv
$ht = #{}
$props = (Get-Content test.csv -TotalCount 1).split(';')
import-csv test.csv -Delimiter ';' |
foreach {
if ( $ht.ContainsKey($_.employeenumber) )
{
foreach ($prop in $props )
{
if ($_.$prop )
{$ht[$_.employeenumber].$prop = $_.$prop }
}
}
else { $ht[$_.employeenumber] = $_ }
}
$ht.values | sort employeenumber
employeenumber : 99999991
phone : +1324569991
mobile : +234569991
fax : +5234569991
userid : user01
Email : user1#domain.com
employeenumber : 99999992
phone : +1234569992
mobile : +234569992
fax :
userid : user02
Email : user2#domain.com
employeenumber : 99999993
phone : +12345699933
mobile : +234569993
fax : +5234569993
userid : user03
Email : user3#domain.com
employeenumber : 99999994
phone : +1234569994
mobile :
fax :
userid : user04
Email : user4#domain.com
Our team has geographically dispersed and many virtual machine will be connected by them using remote desktop. I would like to find who is accessing a remote desktop session and how long it is being used.
I tried to do it with powershell. I wrote a script where user will invoke mstsc using powershell. It will log who has logged in and when he logged. But i would like to find when some one log off from mstsc or disconnect mstsc . Is there any way to capture that information in log file using powershell. Whether any event will be triggered while closing mstsc which could be used for it?
I wrote a PowerShell module,PSTerminalServices (http://psterminalservices.codeplex.com), that is built on Cassia.
Here's a sample command output:
PS> Get-TSSession | fl *
IPAddress :
State : Active
ApplicationName :
Local : False
RemoteEndPoint :
InitialProgram :
WorkingDirectory :
ClientProtocolType : Console
ClientProductId : 0
ClientHardwareId : 0
ClientDirectory :
ClientDisplay : Cassia.Impl.ClientDisplay
ClientBuildNumber : 0
Server : Cassia.Impl.TerminalServer
ClientIPAddress :
WindowStationName : Console
DomainName : homelab
UserAccount : homelab\shay
ClientName :
ConnectionState : Active
ConnectTime : 12/15/2011 2:47:02 PM
CurrentTime : 12/23/2011 4:35:21 PM
DisconnectTime :
LastInputTime :
LoginTime : 12/15/2011 3:11:58 PM
IdleTime : 00:00:00
SessionId : 1
UserName : shay
You could use Cassia to get rdp session information (which could be periodically logged to a log file).
Here's a quick example of how to use cassia in Powershell:
[reflection.assembly]::loadfile("d:\cassia.dll")
$manager = new-object Cassia.TerminalServicesManager
$server = $manager.GetRemoteServer("<name of your server>")
$server.open()
$server.getsessions()
It will return something like this (for every session):
ClientDisplay : Cassia.Impl.ClientDisplay
ClientBuildNumber : 0
Server : Cassia.Impl.TerminalServer
ClientIPAddress :
WindowStationName :
DomainName : CONTOSO
UserAccount : CONTOSO\admin
ClientName :
ConnectionState : Disconnected
ConnectTime : 22/12/2011 19:02:00
CurrentTime : 23/12/2011 9:00:42
DisconnectTime : 22/12/2011 22:22:35
LastInputTime : 22/12/2011 22:22:35
LoginTime : 22/12/2011 10:40:21
IdleTime : 10:38:06.4220944
SessionId : 33
UserName : admin
If you can establish an RPC connexion with the server itself you can use QWinsta.exe to see who is logon a TS and RWinsta.exe to remote close a connexion (see Managing Terminal Services Sessions Remotely)
I run this function once per 15 minutes, it relies on Module PSTerminalServices. Basically what it does, is it pulls the last time someone RDPed in, then stores it in an XML, overwritting an older value if it exists, if no one is currently logged on, it returns the latest value from the XML instead.
Function Get-LastLogonTime
{
<#
.SYNOPSIS
Get-LastLogonTime returns the last date that someone logged on to a computer.
.DESCRIPTION
Get-LastLogonTime returns the last date that someone logged to a computer.
If admin rights are missing on the server it will return False.
.EXAMPLE
Get-LastLogonTime "nameofcomputer"
.NOTES
gets last access time from the user folder
.LINK
http://winfred.com
#>
Param(
[Parameter(Position=0, Mandatory=$true)]$ComputerName
)
$StoredRDPSessions = Import-Clixml "RDPSessions.xml"
$myobj = "" | select ComputerName, LastAccessedDate, UserName
$myobj.ComputerName = $ComputerName
$LastConnectedUser = Get-TSSession -ComputerName $ComputerName | where `
{
($_.WindowStationName -ne "Services") -and `
($_.State -ne "Listening") -and `
($_.WindowStationName -ne "Console")
} | sort-object -property LastAccessTime -Descending
if($LastConnectedUser -is [array])
{
$myobj.LastAccessedDate = $LastConnectedUser[0].ConnectTime
$myobj.UserName = $LastConnectedUser[0].UserName
}elseif($LastConnectedUser){
$myobj.LastAccessedDate = $LastConnectedUser.ConnectTime
$myobj.UserName = $LastConnectedUser.UserName
}else{
$myobj.LastAccessedDate = $Null
$myobj.UserName = "Unknown"
}
if(($myobj.LastAccessedDate) -and ($myobj.UserName))
{
$StoredRDPSession = $StoredRDPSessions | where {$_.ComputerName -eq $ComputerName}
if($StoredRDPSession)
{
if($myobj.LastAccessedDate -gt $StoredRDPSession.LastAccessedDate)
{
write-verbose "Newer LastAccessedDate, updating XML"
$StoredRDPSession.LastAccessedDate = $myobj.LastAccessedDate
$StoredRDPSession.UserName = $myobj.UserName
$StoredRDPSessions | Export-Clixml "RDPSessions.xml"
}
}else{
write-verbose "No Entry found Adding to XML"
$NewStoredRDPSessions = #()
$StoredRDPSessions | % {$NewStoredRDPSessions += $_}
$NewStoredRDPSessions += $myobj
$NewStoredRDPSessions | Export-Clixml "RDPSessions.xml"
}
}
if((!($myobj.LastAccessedDate)) -and $StoredRDPSessions)
{
write-verbose "no current session, pulling from stored XML"
$StoredRDPSession = $StoredRDPSessions | where {$_.ComputerName -eq $ComputerName}
if($StoredRDPSession)
{
$myobj.LastAccessedDate = $StoredRDPSession.LastAccessedDate
$myobj.UserName = $StoredRDPSession.UserName
}else{
write-verbose "Sadness, nothing stored in XML either."
}
}
write-verbose "Get-LastLogonTime $ComputerName - $($myobj.LastAccessedDate) - $($myobj.UserName)"
Return $myobj
}