Private/Kinds/Session.ps1
|
# The Session Kind: the Session of the User of the Run, and what it holds. # # A Report described the Target Machine and said of the User only which account the Run # ran under. This Check says who the User of the Run is, since when they are signed in, # and how many processes and how much private memory their Session holds: what tells a # heavy Session from a heavy machine. # # State only (ADR-0006). What a Session holds is a count and a sum. No name of a program # is kept per Session, and nothing about when or how long anything was used. # # Whether the Run is in the Session of the person working at the machine the Run decides, # once, and hands over as $Observed['RunUser']. This Kind does not work it out again. # Where it is not, nothing here is the User's, and the Check says nothing of the User and # no number. That the Run is in the wrong Session it does not say either: the Run says # that, once, with what was therefore not read (New-UserContextFinding). # # The Gatherer reads every Session Windows reports and not only this one, and counts for # each. The Judge speaks of the one the Run is in, and then of all of them: a table of # every Session somebody is signed in to, which on a terminal server is who else is on it, # and a warning for a disconnected one that still holds memory. That part is true of the # machine and is said in the wrong Session too. # # Of other Users what ADR-0006 allows and no more: the account; the state of the Session # with since when it is signed in to and since when it is without input (or disconnected); # and how many processes and how much private memory it holds. Never which programs. # # What the data keeps of a Session is, to that end: its number, the account, the state, # the three times Windows gives (signed in, last input, last disconnect), and one yes or # no - whether Windows said how the Session is connected, which tells a Session it # refused to answer for from one nobody is signed in to. Not how it is connected, and not # from where. See ConvertTo-SessionEntry. Beside them, for each Session, the count and # the sum: see Group-SessionProcess. # # What Windows does not answer a User about the Session of another account, the # SessionElevated Check read with admin rights, where the Elevated Part ran. Its reading # fills in. # # Get-SessionData reads the machine and decides nothing. ConvertTo-SessionRow, # ConvertTo-SessionFinding, ConvertTo-SessionSection and Get-SessionTopRow are pure. # What Windows calls the states of a Session, by their number: WTS_CONNECTSTATE_CLASS, as # Microsoft documents it for wtsapi32. Carried as the name, so that the data reads the # same on every machine; what a state is called in the Report is not decided here. $script:SessionStateName = @( 'Active', 'Connected', 'ConnectQuery', 'Shadow', 'Disconnected', 'Idle', 'Listen', 'Reset', 'Down', 'Init' ) # The states nobody is signed in in: a listener, and a Session being made or taken down. # A Session in one of them that has no account is not one Windows refused to answer for. $script:SessionNoUserState = @('Listen', 'Idle', 'Reset', 'Down', 'Init') # WTS_INFO_CLASS: the one class that carries the times of a Session, WTSSessionInfo. $script:SessionInfoClass = 24 function Initialize-SessionNativeMethod { <# .SYNOPSIS Defines the calls into wtsapi32 the Session Gatherer needs, once per session. .DESCRIPTION Called from the Gatherer and not at import, for the reason Initialize-AppNativeMethod gives: importing this module defines functions and does nothing else. Windows has no cmdlet that gives the time a Session was signed in to. quser prints it, in the language and the date format of the machine, and is not on every edition of Windows. #> [CmdletBinding()] param() if ('Gutcheck.SessionNative' -as [type]) { return } Add-Type -Namespace Gutcheck -Name SessionNative -MemberDefinition @' [DllImport("wtsapi32.dll", EntryPoint = "WTSEnumerateSessionsW", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool WTSEnumerateSessions(IntPtr server, int reserved, int version, out IntPtr sessions, out int count); [DllImport("wtsapi32.dll", EntryPoint = "WTSQuerySessionInformationW", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool WTSQuerySessionInformation(IntPtr server, int sessionId, int infoClass, out IntPtr buffer, out int bytes); [DllImport("wtsapi32.dll")] public static extern void WTSFreeMemory(IntPtr memory); [StructLayout(LayoutKind.Sequential)] public struct SessionEntry { public int SessionId; public IntPtr StationName; public int State; } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] public struct SessionInfo { public int State; public int SessionId; public int IncomingBytes; public int OutgoingBytes; public int IncomingFrames; public int OutgoingFrames; public int IncomingCompressedBytes; public int OutgoingCompressedBytes; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] public string StationName; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 17)] public string Domain; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 21)] public string UserName; public long ConnectTime; public long DisconnectTime; public long LastInputTime; public long LogonTime; public long CurrentTime; } '@ } function ConvertFrom-SessionFileTime { <# .SYNOPSIS A time as Windows gives it for a Session, as a local time; nothing where it gave none, which it says with zero. Pure. #> [CmdletBinding()] param([AllowNull()]$FileTime) if ($null -eq $FileTime -or [long]$FileTime -le 0) { return $null } try { [datetime]::FromFileTime([long]$FileTime) } catch { $null } } function Get-SessionReading { <# .SYNOPSIS Every Session Windows reports on this machine, as it reports it. Decides nothing. .DESCRIPTION One row per Session: its number, the account, the state by its name, whether Windows said how it is connected, when it was signed in to, when it last had input and when it was last disconnected. What Windows does not give is absent: the account of a Session nobody is signed in to (the services, the listener for Remote Desktop), the last input of a Session at the machine itself, and everything but the number and the state of a Session this account may not ask about. The time of the last disconnect is kept by Windows after the Session is connected again: it says since when a Session is disconnected only of one that is. The Session this process is in is always asked for, whether or not Windows listed it. #> [CmdletBinding()] [OutputType([psobject])] param([AllowNull()]$OwnSessionId) try { Initialize-SessionNativeMethod } catch { return @() } $states = @{} $ids = New-Object System.Collections.Generic.List[int] $list = [IntPtr]::Zero $count = 0 try { if ([Gutcheck.SessionNative]::WTSEnumerateSessions([IntPtr]::Zero, 0, 1, [ref]$list, [ref]$count)) { $size = [Runtime.InteropServices.Marshal]::SizeOf([type][Gutcheck.SessionNative+SessionEntry]) for ($i = 0; $i -lt $count; $i++) { $entry = [Runtime.InteropServices.Marshal]::PtrToStructure( [IntPtr]($list.ToInt64() + $i * $size), [type][Gutcheck.SessionNative+SessionEntry]) $ids.Add($entry.SessionId) $states[$entry.SessionId] = $entry.State } } } catch { } finally { if ($list -ne [IntPtr]::Zero) { [Gutcheck.SessionNative]::WTSFreeMemory($list) } } if ($null -ne $OwnSessionId -and -not $ids.Contains([int]$OwnSessionId)) { $ids.Add([int]$OwnSessionId) } @(foreach ($id in $ids) { $account = $null; $station = $null; $signedIn = $null; $lastInput = $null; $disconnected = $null $state = $states[$id] $buffer = [IntPtr]::Zero $bytes = 0 try { if ([Gutcheck.SessionNative]::WTSQuerySessionInformation([IntPtr]::Zero, $id, $script:SessionInfoClass, [ref]$buffer, [ref]$bytes)) { $info = [Runtime.InteropServices.Marshal]::PtrToStructure($buffer, [type][Gutcheck.SessionNative+SessionInfo]) if ($info.UserName) { $account = $(if ($info.Domain) { '{0}\{1}' -f $info.Domain, $info.UserName } else { $info.UserName }) } $station = $info.StationName $state = $info.State $signedIn = ConvertFrom-SessionFileTime $info.LogonTime $lastInput = ConvertFrom-SessionFileTime $info.LastInputTime $disconnected = ConvertFrom-SessionFileTime $info.DisconnectTime } } catch { } finally { if ($buffer -ne [IntPtr]::Zero) { [Gutcheck.SessionNative]::WTSFreeMemory($buffer) } } $stateName = $null if ($null -ne $state -and $state -ge 0 -and $state -lt $script:SessionStateName.Count) { $stateName = $script:SessionStateName[$state] } # How the Session is connected is read to tell whether Windows answered for it, # and is not kept: see ConvertTo-SessionEntry. ConvertTo-SessionEntry -Session ([pscustomobject]@{ SessionId = $id Account = $account State = $stateName Station = $station SignedInAt = $signedIn LastInputAt = $lastInput DisconnectedAt = $disconnected }) }) } function ConvertTo-SessionEntry { <# .SYNOPSIS What is kept of one Session as Windows reports it: its number, the account, the state, three times, and whether Windows said how it is connected. Pure. .DESCRIPTION ADR-0006 in code, for what the Gatherers of both Parts keep. Built here property by property, whatever a row carries on its way in. How a Session is connected - "Console", "RDP-Tcp#4", which names the listener and numbers the connection - is not kept. It is needed for one thing: a Session Windows refused to answer for has none, and one nobody is signed in to has one (see Test-SessionUnread). So what is kept of it is a yes or no, HasStation. #> [CmdletBinding()] [OutputType([psobject])] param([AllowNull()]$Session) if ($null -eq $Session) { return } [pscustomobject]@{ SessionId = Get-DataProperty $Session 'SessionId' Account = Get-DataProperty $Session 'Account' State = Get-DataProperty $Session 'State' HasStation = [bool]([bool](Get-DataProperty $Session 'HasStation') -or "$(Get-DataProperty $Session 'Station')".Trim()) SignedInAt = Get-DataProperty $Session 'SignedInAt' LastInputAt = Get-DataProperty $Session 'LastInputAt' DisconnectedAt = Get-DataProperty $Session 'DisconnectedAt' } } function Get-SessionProcess { <# .SYNOPSIS Every running process as the Session it is in and the private memory it holds, and nothing else of it. Decides nothing. .DESCRIPTION No name and no path: which programs a User runs is not what this Check reports (ADR-0006), so it is not read into its data either. Windows gives both numbers of the processes of other accounts too, without admin rights. #> [CmdletBinding()] [OutputType([psobject])] param() @(Get-Process -ErrorAction SilentlyContinue | ForEach-Object { [pscustomobject]@{ SessionId = $_.SessionId; PrivateBytes = $_.PrivateMemorySize64 } }) } function Group-SessionProcess { <# .SYNOPSIS What the processes of each Session hold together: how many, and their private memory in MB. Pure. .DESCRIPTION A number, a count and a sum for each Session. Whatever else a row carried is not carried on. #> [CmdletBinding()] [OutputType([psobject])] param([AllowNull()][AllowEmptyCollection()]$Process) @($Process | Where-Object { $null -ne $_ -and $null -ne (Get-DataProperty $_ 'SessionId') } | Group-Object { [int](Get-DataProperty $_ 'SessionId') } | ForEach-Object { $bytes = (@($_.Group | ForEach-Object { ConvertTo-Number (Get-DataProperty $_ 'PrivateBytes') } | Where-Object { $null -ne $_ }) | Measure-Object -Sum).Sum if ($null -eq $bytes) { $bytes = 0 } [pscustomobject]@{ SessionId = [int](Get-DataProperty $_.Group[0] 'SessionId') Processes = @($_.Group).Count PrivateMB = [long][math]::Round($bytes / 1MB) } } | Sort-Object SessionId) } function Get-SessionData { <# .SYNOPSIS The Sessions of the machine, what the processes of each hold, which of them this Run is in, and what the Run said about whose that is. Decides nothing. .PARAMETER Observed What the Run knows before this Check. 'RunUser' is the account the Run runs as, the one working at the machine, and whether they differ: see ConvertTo-RunUserObserved. Copied across as it came. 'SessionElevated' is the same Sessions and what they hold as an administrator may read them, absent where that Check was not performed. #> [CmdletBinding()] [OutputType([psobject])] param([hashtable]$Parameters = @{}, [AllowNull()][hashtable]$Observed = @{}) $runUser = $null if ($Observed -and $Observed.ContainsKey('RunUser')) { $runUser = $Observed['RunUser'] } # Copied across as it came. Which of it fills in what is for the pure half to say. $elevated = $null if ($Observed -and $Observed.ContainsKey('SessionElevated')) { $elevated = $Observed['SessionElevated'] } $own = $null try { $own = (Get-Process -Id $PID -ErrorAction Stop).SessionId } catch { } [pscustomobject]@{ PSTypeName = 'Gutcheck.Data.Session' Sessions = @(Get-SessionReading -OwnSessionId $own | ForEach-Object { ConvertTo-SessionEntry -Session $_ }) Held = @(Group-SessionProcess -Process (Get-SessionProcess)) OwnSessionId = $own RunUser = $runUser GatheredAt = Get-Date ElevatedRead = ($null -ne $elevated) ElevatedSessions = @((Get-DataCollection $elevated 'Sessions') | ForEach-Object { $_ } | ForEach-Object { ConvertTo-SessionEntry -Session $_ }) ElevatedHeld = @((Get-DataCollection $elevated 'Held') | ForEach-Object { $_ }) } } function ConvertTo-SessionTime { <# .SYNOPSIS A time of a Session as a time, whichever way it reached here, and nothing as nothing. Pure. .DESCRIPTION Read the one way a Judge reads a time, and never by a free parse: see ConvertTo-DataTime. #> [CmdletBinding()] param([AllowNull()]$Value) if ($null -eq $Value) { return $null } ConvertTo-DataTime $Value } function Test-SessionUnread { <# .SYNOPSIS Whether Windows listed this Session and did not answer for it. Pure. .DESCRIPTION Told apart from a Session nobody is signed in to, which has no account either: of that one Windows gives how it is connected ("Services", "Console", the name of the listener), and the data holds that it did: HasStation, and not what it gave. Of a Session it refused to answer for it gives the number and the state and nothing else. The services have the number 0 on every Windows since Vista, and nobody is signed in to a listener, whatever was read of them. #> [CmdletBinding()] [OutputType([bool])] param([AllowNull()]$Session) if ($null -eq $Session) { return $false } if ("$(Get-DataProperty $Session 'Account')".Trim()) { return $false } if ([bool](Get-DataProperty $Session 'HasStation')) { return $false } if ($null -ne (Get-DataProperty $Session 'SignedInAt') -or $null -ne (Get-DataProperty $Session 'LastInputAt')) { return $false } if ("$(Get-DataProperty $Session 'SessionId')" -eq '0') { return $false } "$(Get-DataProperty $Session 'State')" -notin $script:SessionNoUserState } function ConvertTo-SessionReading { <# .SYNOPSIS The Sessions of one reading, each with what its processes hold beside it: the shape both readings are brought to before one fills in the other. Pure. .DESCRIPTION One row for each Session the reading has: its number as text, the account and the state or nothing, the three times as times, how many processes and how much private memory where a process of it was counted, and Unread: whether Windows listed the Session and did not answer for it (see Test-SessionUnread). Nothing is left out here and nothing decided: which Sessions are somebody's is for ConvertTo-SessionRow. Not how a Session is connected either, which a row may carry on its way in. #> [CmdletBinding()] [OutputType([psobject])] param([AllowNull()][AllowEmptyCollection()]$Session, [AllowNull()][AllowEmptyCollection()]$Held) $holds = @{} foreach ($entry in @($Held | Where-Object { $null -ne $_ })) { $holds["$(Get-DataProperty $entry 'SessionId')"] = $entry } foreach ($entry in @($Session | Where-Object { $null -ne $_ })) { $id = "$(Get-DataProperty $entry 'SessionId')" $account = "$(Get-DataProperty $entry 'Account')".Trim() $state = "$(Get-DataProperty $entry 'State')".Trim() [pscustomobject]@{ SessionId = $id Account = $(if ($account) { $account } else { $null }) State = $(if ($state) { $state } else { $null }) SignedInAt = ConvertTo-SessionTime (Get-DataProperty $entry 'SignedInAt') LastInputAt = ConvertTo-SessionTime (Get-DataProperty $entry 'LastInputAt') DisconnectedAt = ConvertTo-SessionTime (Get-DataProperty $entry 'DisconnectedAt') Processes = ConvertTo-Number (Get-DataProperty $holds[$id] 'Processes') PrivateMB = ConvertTo-Number (Get-DataProperty $holds[$id] 'PrivateMB') Unread = Test-SessionUnread -Session $entry } } } function ConvertTo-SessionRow { <# .SYNOPSIS Every Session somebody is signed in to, with what its processes hold, and what this account could not read of it filled in from the reading taken with admin rights. Pure. .DESCRIPTION A Session nobody is signed in to is left out: the services, the listener for Remote Desktop, a console showing the sign-in screen. A Session Windows listed and did not answer for stays, as Unread: it is somebody's. Only what was unread is filled in, and only from the same Session: the same number and, where this account read one, the same account, as a number is given to another Session once its first has ended. What this account read itself stays, as it was read later. A Session only the reading with admin rights has is not added: it has ended since. One of which that reading says nobody is signed in is left out. The filling in itself is Join-ElevatedReading's, which the Program Kind fills in by as well; what a Session holds is filled in with it, as part of its row. IdleSince is when the Session last had input. Of a disconnected Session it is when it was disconnected, where that is later or Windows gave no time of the last input. Of any other, the time of an earlier disconnect says nothing. This is where ADR-0006 is held in code: the account, the state, two times, a count and a sum, and whether it is the Run's. Whatever else a row carried is not carried on, how the Session is connected and from where included. #> [CmdletBinding()] [OutputType([psobject])] param([AllowNull()]$Data) $own = Get-DataProperty $Data 'OwnSessionId' # Both readings in one shape, and of this account's the Sessions that are somebody's. $mine = @(ConvertTo-SessionReading -Session (Get-DataCollection $Data 'Sessions') -Held (Get-DataCollection $Data 'Held') | Where-Object { $_.Account -or $_.Unread }) $theirs = @(ConvertTo-SessionReading -Session (Get-DataCollection $Data 'ElevatedSessions') -Held (Get-DataCollection $Data 'ElevatedHeld')) # The same Session still: by the reading with admin rights somebody is signed in to # it, and that is the account this one read, where it read one. $same = { param($Session, $Other) $Other.Account -and (-not $Session.Account -or [string]::Equals($Session.Account, $Other.Account, [StringComparison]::OrdinalIgnoreCase)) } $rows = @(Join-ElevatedReading -Row $mine -Elevated $theirs -Key 'SessionId' -Same $same ` -Take 'Account', 'State', 'SignedInAt', 'LastInputAt', 'DisconnectedAt', 'Processes', 'PrivateMB' | ForEach-Object { $session = $_.Row $other = $_.Elevated # With admin rights it was read, and nobody is signed in to it. if ($session.Unread -and $null -ne $other -and -not $other.Account -and -not $other.Unread) { return } $idle = $session.LastInputAt if ($session.State -eq 'Disconnected') { $disconnected = $session.DisconnectedAt if ($null -ne $disconnected -and ($null -eq $idle -or $disconnected -gt $idle)) { $idle = $disconnected } } $id = $session.SessionId [pscustomobject]@{ SessionId = $(if ($id -match '^\d+$') { [long]$id } else { $id }) Account = $session.Account State = $session.State SignedInAt = $session.SignedInAt IdleSince = $idle Processes = $(if ($null -ne $session.Processes) { [long]$session.Processes } else { $null }) PrivateMB = $(if ($null -ne $session.PrivateMB) { [long]$session.PrivateMB } else { $null }) # Unread still, where the reading with admin rights gave no account either. Unread = [bool]($session.Unread -and -not $session.Account) Own = [bool]($null -ne $own -and "$own" -eq $id) } }) # Three keys, so that the order does not depend on how a sort treats equals, nor on # the order Windows listed the Sessions in. @($rows | Sort-Object @{ Expression = { if ($null -ne $_.PrivateMB) { $_.PrivateMB } else { [long]-1 } }; Descending = $true }, @{ Expression = { "$($_.Account)".ToLowerInvariant() }; Descending = $false }, @{ Expression = 'SessionId'; Descending = $false }) } function Get-SessionStateText { <# .SYNOPSIS What the Report calls the state of a Session. Pure. .DESCRIPTION A state this list has no word for - Windows may gain one - is said to be unknown, and the name Windows has for it does not stand bare in a Report. #> [CmdletBinding()] [OutputType([string])] param([AllowNull()][AllowEmptyString()][string]$State) switch ("$State".Trim()) { 'Active' { return Get-Text 'Value.Session.State.Active' } 'Connected' { return Get-Text 'Value.Session.State.Connected' } 'ConnectQuery' { return Get-Text 'Value.Session.State.ConnectQuery' } 'Shadow' { return Get-Text 'Value.Session.State.Shadow' } 'Disconnected' { return Get-Text 'Value.Session.State.Disconnected' } 'Idle' { return Get-Text 'Value.Session.State.Idle' } 'Listen' { return Get-Text 'Value.Session.State.Listen' } 'Reset' { return Get-Text 'Value.Session.State.Reset' } 'Down' { return Get-Text 'Value.Session.State.Down' } 'Init' { return Get-Text 'Value.Session.State.Init' } default { return Get-Text 'Value.Session.State.Unknown' } } } function Get-SessionOfRun { <# .SYNOPSIS The Session the Run is in, with what its processes hold. Pure. .DESCRIPTION Always an answer, so that what Windows did not give is said to be unread and not left out: SignedInAt is absent where Windows gave no time or did not report the Session, Processes and PrivateMB where no process of it was counted. The account is the one Windows gives for the Session, and the one the Run runs as where it gives none. They are the same account: this is asked only where the Run said it is in the Session of the person working at the machine. #> [CmdletBinding()] [OutputType([psobject])] param([AllowNull()]$Data) $id = Get-DataProperty $Data 'OwnSessionId' $session = $null $held = $null if ($null -ne $id) { $session = (Get-DataCollection $Data 'Sessions') | ForEach-Object { $_ } | Where-Object { $null -ne $_ -and "$(Get-DataProperty $_ 'SessionId')" -eq "$id" } | Select-Object -First 1 $held = (Get-DataCollection $Data 'Held') | ForEach-Object { $_ } | Where-Object { $null -ne $_ -and "$(Get-DataProperty $_ 'SessionId')" -eq "$id" } | Select-Object -First 1 } $account = "$(Get-DataProperty $session 'Account')".Trim() if (-not $account) { $account = "$(Get-DataProperty (Get-DataProperty $Data 'RunUser') 'RunningAs')".Trim() } $signedIn = ConvertTo-SessionTime (Get-DataProperty $session 'SignedInAt') [pscustomobject]@{ Account = $account SignedInAt = $signedIn Processes = ConvertTo-Number (Get-DataProperty $held 'Processes') PrivateMB = ConvertTo-Number (Get-DataProperty $held 'PrivateMB') } } function Get-SessionAge { <# .SYNOPSIS How long ago a Session was signed in to, when the machine was read: the whole days, and the same in words. Nothing where either time is missing. Pure. #> [CmdletBinding()] [OutputType([psobject])] param([AllowNull()]$SignedInAt, [AllowNull()]$GatheredAt) $from = ConvertTo-SessionTime $SignedInAt $until = ConvertTo-SessionTime $GatheredAt if ($null -eq $from -or $null -eq $until) { return $null } $span = $until - $from $days = [int][math]::Floor($span.TotalDays) $hours = [int][math]::Floor($span.TotalHours) $text = $( if ($hours -lt 1) { Get-Text 'Value.Session.Age.Now' } elseif ($days -lt 1) { (Get-Text 'Value.Session.Age.Hours') -f $hours } elseif ($days -eq 1) { Get-Text 'Value.Session.Age.Day' } else { (Get-Text 'Value.Session.Age.Days') -f $days } ) [pscustomobject]@{ Days = [math]::Max(0, $days); Text = $text } } function ConvertTo-SessionFinding { <# .SYNOPSIS Who the User of the Run is and since when signed in, and what their Session holds. Pure. .DESCRIPTION SignedInInfoAfterDays (7) is a rule of thumb and no limit of Windows or of Microsoft: a working week. A Session signed in to for longer than that has programs in it that have run as long, and that is worth a sentence. It is information however long it has been, and never a warning: how long the machine itself has run has a Finding of its own, and the Report is not to warn twice about one thing. There is no limit on processes or memory of the User's own Session. What is much for a Session depends on the machine, which the memory Check judges; here it is stated. A disconnected Session warns where it is idle for longer than DisconnectedIdleWarnDays (1) AND holds more than DisconnectedMemoryWarnMB (1024). Both are rules of thumb and neither is a limit of Windows or one Microsoft documents: Windows keeps a disconnected Session for as long as nobody signs it off or a policy does ("Set time limit for disconnected sessions", which is not set by default). A day, because a Session disconnected over lunch or over night is how people work and one left over a whole working day is forgotten. 1 GB, because below that it is not what makes a terminal server slow. Both are to be looked at again after the first Reports from Customer Sites (#59). What is said of all Sessions is true of the machine and is said where the Run is not in the Session of the User, too. That the User has a second Session is not: there, the account of the Run is not the User's. Where the Run is not in the Session of the User, that is all: no Finding about the User, and none saying that the Run is in the wrong Session. The Run says that once, in the warning about the wrong account, which names the Session among what was not read (see New-UserContextFinding). A Report said it six times, and a Technician reads none of six. #> [CmdletBinding()] [OutputType([psobject])] param([AllowNull()]$Data, [hashtable]$Parameters = @{}) $infoDays = ConvertTo-Number (Get-Parameter $Parameters 'SignedInInfoAfterDays' 7) if ($null -eq $infoDays) { $infoDays = 7 } $idleDays = ConvertTo-Number (Get-Parameter $Parameters 'DisconnectedIdleWarnDays' 1) if ($null -eq $idleDays) { $idleDays = 1 } $memoryMB = ConvertTo-Number (Get-Parameter $Parameters 'DisconnectedMemoryWarnMB' 1024) if ($null -eq $memoryMB) { $memoryMB = 1024 } $who = Get-Text 'Check.Session.User' if ($null -eq $Data) { return New-UnavailableFinding -Category User -Check $who -Hint (Get-Text 'Hint.Session.Repeat') } # Decided by the Run, and only read here. Said by the Run as well, once: the warning # about the wrong account names what was therefore not read (New-UserContextFinding). # So nothing is said of it here, and nothing of the User: only what is true of the # machine. $runUser = Get-DataProperty $Data 'RunUser' if ([bool](Get-DataProperty $runUser 'Differs')) { Get-SessionMachineFinding -Data $Data -IdleDays $idleDays -MemoryMB $memoryMB return } $session = Get-SessionOfRun -Data $Data $value = @() if ($session.Account) { $value += $session.Account } if ($null -eq $session.SignedInAt) { $value += Get-Text 'Value.Session.SignedInUnread' New-Finding -Category User -Check $who -Severity INFO -Value ($value -join ' | ') ` -Meaning (Get-Text 'Meaning.Session.SignedInUnread') -Hint (Get-Text 'Hint.Session.Repeat') } else { $value += (Get-Text 'Value.Session.SignedInSince') -f $session.SignedInAt $age = Get-SessionAge -SignedInAt $session.SignedInAt -GatheredAt (Get-DataProperty $Data 'GatheredAt') if ($age) { $value += $age.Text } $long = [bool]($age -and $age.Days -ge $infoDays) New-Finding -Category User -Check $who -Severity $(if ($long) { 'INFO' } else { 'OK' }) -Value ($value -join ' | ') ` -Meaning ((Get-Text 'Meaning.Session.SignedInLong') -f $infoDays) -Hint (Get-Text 'Hint.Session.SignedInLong') } $holds = Get-Text 'Check.Session.Holds' if ($null -eq $session.Processes -or $null -eq $session.PrivateMB) { # Not a zero: a Session the Run is in holds at least the Run. New-Finding -Category User -Check $holds -Severity INFO -Value (Get-Text 'Value.Session.HoldsUnread') ` -Meaning (Get-Text 'Meaning.Session.HoldsUnread') -Hint (Get-Text 'Hint.Session.Repeat') } else { New-Finding -Category User -Check $holds -Severity OK ` -Value ((Get-Text 'Value.Session.Holds') -f [long]$session.Processes, (Format-DataSize -MB $session.PrivateMB)) } Get-SessionMachineFinding -Data $Data -IdleDays $idleDays -MemoryMB $memoryMB -UserAccount $session.Account } function Get-SessionMachineFinding { <# .SYNOPSIS What is to be said of all Sessions of the machine: a warning for each disconnected one that holds memory, a note for a second Session of the User of the Run, and one Finding saying how many there are. Pure. .DESCRIPTION The limits are the Judge's, which hands them over: see ConvertTo-SessionFinding. A Session whose account and times could not be read is counted and said to be unread, and is not judged: since when it is idle is not known. .PARAMETER UserAccount The account of the User of the Run. Absent where the Run is not in the Session of the User: a second Session is then not looked for. #> [CmdletBinding()] [OutputType([psobject])] param([AllowNull()]$Data, [double]$IdleDays = 1, [double]$MemoryMB = 1024, [AllowNull()][AllowEmptyString()][string]$UserAccount) $rows = @(ConvertTo-SessionRow -Data $Data) $read = @($rows | Where-Object { -not $_.Unread }) $gatheredAt = ConvertTo-SessionTime (Get-DataProperty $Data 'GatheredAt') $over = New-Object System.Collections.Generic.List[string] foreach ($row in $read) { if ($row.State -ne 'Disconnected' -or $null -eq $row.IdleSince -or $null -eq $row.PrivateMB -or $null -eq $gatheredAt) { continue } if (($gatheredAt - $row.IdleSince).TotalDays -le $IdleDays -or $row.PrivateMB -le $MemoryMB) { continue } $over.Add($row.Account) $value = @($row.Account, ((Get-Text 'Value.Session.Disconnected') -f $row.IdleSince)) $age = Get-SessionAge -SignedInAt $row.IdleSince -GatheredAt $gatheredAt if ($age) { $value += $age.Text } $value += (Get-Text 'Value.Session.Holds') -f $(if ($null -ne $row.Processes) { $row.Processes } else { 0 }), (Format-DataSize -MB $row.PrivateMB) New-Finding -Category User -Check ((Get-Text 'Check.Session.Disconnected') -f $row.Account) -Severity WARN ` -Subject $row.Account -Value ($value -join ' | ') ` -Meaning ((Get-Text 'Meaning.Session.Disconnected') -f $IdleDays, (Format-DataSize -MB $MemoryMB)) ` -Hint ((Get-Text 'Hint.Session.Disconnected') -f $row.Account) } if ("$UserAccount".Trim()) { foreach ($row in @($read | Where-Object { -not $_.Own -and [string]::Equals("$($_.Account)", "$UserAccount".Trim(), [StringComparison]::OrdinalIgnoreCase) })) { $value = @("$UserAccount".Trim(), ((Get-Text 'Value.Session.Second') -f (Get-SessionStateText -State $row.State))) if ($null -ne $row.SignedInAt) { $value += (Get-Text 'Value.Session.SignedInSince') -f $row.SignedInAt } if ($null -ne $row.Processes -and $null -ne $row.PrivateMB) { $value += (Get-Text 'Value.Session.Holds') -f $row.Processes, (Format-DataSize -MB $row.PrivateMB) } New-Finding -Category User -Check (Get-Text 'Check.Session.Second') -Severity INFO -Value ($value -join ' | ') ` -Meaning (Get-Text 'Meaning.Session.Second') -Hint (Get-Text 'Hint.Session.Second') } } $summary = @((Get-Text 'Value.Session.Count') -f $read.Count) if ($read.Count) { $summary += (Get-Text 'Value.Session.CountByState') -f @($read | Where-Object { $_.State -eq 'Active' }).Count, @($read | Where-Object { $_.State -eq 'Disconnected' }).Count } if ($over.Count) { $summary += (Get-Text 'Value.Session.DisconnectedOver') -f $over.Count, ($over -join ', ') } else { $summary += Get-Text 'Value.Session.NoneDisconnectedOver' } # In the Value and not as a Meaning: an OK Finding carries none, and this has to be # said when everything that was read is in order too. $unread = @($rows | Where-Object { $_.Unread }) if ($unread.Count) { # Whose fault it is that they are unread: nobody asked with admin rights, or # Windows did not give them to an administrator either. if ((Get-UnreadReason $Data) -eq 'Refused') { $summary += (Get-Text 'Value.Session.UnreadProtected') -f $unread.Count } else { $summary += (Get-Text 'Value.Session.Unread') -f $unread.Count } } New-Finding -Category User -Check (Get-Text 'Check.Session.All') -Severity OK -Value ($summary -join ' | ') } function ConvertTo-SessionSection { <# .SYNOPSIS Every Session somebody is signed in to as a table, the User's own included. Pure. .DESCRIPTION True of the machine, and shown whichever Session the Run is in. No cell is empty: what Windows gave no time for is said in words, and of a Session this account could not read a cell says "nicht gelesen" where nobody read with admin rights in this Run and "nicht lesbar" where Windows did not give it with admin rights either (see Get-UnreadText), with the same at length in the last column. Of a Session that was read, what its processes hold is "nicht lesbar" where no process of it was counted: that was asked for, and Windows gave none. #> [CmdletBinding()] [OutputType([psobject])] param([AllowNull()]$Data) $unread = Get-UnreadText -Reason (Get-UnreadReason $Data) $notGiven = Get-Text 'Value.Session.NotGiven' $protected = (Get-UnreadReason $Data) -eq 'Refused' New-Section -Title (Get-Text 'Title.Session.All') -Row @( ConvertTo-SessionRow -Data $Data | ForEach-Object { $note = @() if ($_.Own) { $note += Get-Text 'Value.Session.Note.Own' } if ($_.Unread -and $protected) { $note += Get-Text 'Value.Session.Note.Protected' } elseif ($_.Unread) { $note += Get-Text 'Value.Session.Note.Unread' } $row = [ordered]@{} $row[(Get-Text 'Column.Session.Account')] = $(if ($_.Account) { $_.Account } else { (Get-Text 'Value.Session.AccountUnread') -f $unread, $_.SessionId }) $row[(Get-Text 'Column.Session.State')] = Get-SessionStateText -State $_.State $row[(Get-Text 'Column.Session.SignedInAt')] = $(if ($null -ne $_.SignedInAt) { (Get-Text 'Value.Session.At') -f $_.SignedInAt } elseif ($_.Unread) { $unread } else { $notGiven }) $row[(Get-Text 'Column.Session.IdleSince')] = $(if ($null -ne $_.IdleSince) { (Get-Text 'Value.Session.At') -f $_.IdleSince } elseif ($_.Unread) { $unread } else { $notGiven }) $row[(Get-Text 'Column.Session.Processes')] = $(if ($null -ne $_.Processes) { $_.Processes } else { $unread }) $row[(Get-Text 'Column.Session.PrivateMB')] = $(if ($null -ne $_.PrivateMB) { $_.PrivateMB } else { $unread }) $row[(Get-Text 'Column.Note')] = $note -join '; ' [pscustomobject]$row } ) } function Get-SessionTopRow { <# .SYNOPSIS Since when the User of the Run is signed in, as a row of the box at the top of the Report: Group, Name, Value. Nothing where the Check was not performed. Pure. .DESCRIPTION A row of the Run, like who ran it and with what rights, and added the way those are: see Get-UserTopRow and Add-TopRow. Where Windows gave no time, the row says so: no row would read as nothing to say. Nothing where the Run is not in the Session of the User. The box says that in one row for everything about the User, which the Run makes: see Get-UserTopRow. #> [CmdletBinding()] [OutputType([psobject])] param([AllowNull()]$Data) if ($null -eq $Data) { return } if ([bool](Get-DataProperty (Get-DataProperty $Data 'RunUser') 'Differs')) { return } $session = Get-SessionOfRun -Data $Data $value = $( if ($null -eq $session.SignedInAt) { Get-Text 'Value.Cell.Unreadable' } else { $age = Get-SessionAge -SignedInAt $session.SignedInAt -GatheredAt (Get-DataProperty $Data 'GatheredAt') ((Get-Text 'Value.Session.TopSince') -f $session.SignedInAt, "$(if ($age) { $age.Text })").TrimEnd(' ', ',') } ) [pscustomobject]@{ Group = (Get-Text 'System.Info.Group.Run'); Name = (Get-Text 'Run.Session.SignedInSince'); Value = $value } } |