Private/Kinds/UserProfileOther.ps1
|
# The other User Profiles of the machine: a part of the UserProfile Kind, and no Kind. # # A file of its own because the UserProfile Kind is three things - the User Profile of # the User, where its folders lie, and every other User Profile on the disk - and one # file of three thousand lines was all of them. What is here is called by # Kinds/UserProfile.ps1 and by Kinds/UserProfileElevated.ps1, and is loaded like every # file under Private. # # No Kind, and it must not become one: a Kind is a file here that defines # Get-<Kind>Data AND ConvertTo-<Kind>Finding (see Initialize-KindVocabulary). This one # defines Get-UserProfileOtherData, which the UserProfile Gatherer calls, and no # ConvertTo-UserProfileOtherFinding: its Finding is Get-UserProfileOtherFinding, made by # the Judge of the UserProfile Kind. A function of that other name here would let a # Check Definition select half a Check. A test holds the vocabulary to not having it. # # No parameter of a Check Definition is read here, for the same reason: the reference # of the Kinds is built file by file, and a limit read in this file would be documented # for a Kind that does not exist. The Judge reads UnusedDays and hands it over. # # What else lives on this disk: every User Profile of a person, with the account, the # kind, the size of the profile folder and when it was last used, and nothing else of # another User (ADR-0006). No folder of it, no file, no number of files. # # What an account without admin rights reads of the User Profile of another (seen on # Windows 11): that it is there, its folder, the marks its kind is known by, whether it # is loaded, and the times Windows keeps of it in the registry. Not its size: the folder # of another account does not open. The UserProfileElevated Check measures that in the # Elevated Part, and its reading fills in here, User Profile by User Profile. # # What is read is not what is kept. A reading is reduced where it is gathered, in both # Parts, by ConvertTo-UserProfileOtherEntry, to what a Report may hold of another User: # the data of neither Check holds where a User Profile lies, where it roams to, or what # Windows keeps of it in the registry. The data of the Elevated Part is written to disk # and handed from one process to the other, and that is no place for more than a Report # may show. # # All of it is true of the machine and is said whichever Session the Run is in. # # Get-UserProfileOtherReading and Get-UserProfileOtherData read the machine and decide # nothing. Everything else here is pure. # Whose User Profile is a person's, by the number of the account: S-1-5-21-... is an # account of the machine or of a domain, S-1-12-1-... one of Entra ID (Microsoft, the # list of well-known SIDs). That leaves out the three of the system, S-1-5-18, -19 and # -20, and the accounts Windows makes for a service or an application pool. $script:UserProfileOtherPersonSid = '^S-1-(5-21|12-1)-' # Where Windows keeps the list of User Profiles in the registry. $script:UserProfileOtherListKey = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList' # What is kept of the kind of the User Profile of another User, and of how its size came # about: closed lists, so that neither can carry anything else. 'Unread' is what was not # read; the other states of a size are those of the measuring. $script:UserProfileOtherKind = @('Local', 'Roaming', 'Mandatory', 'Temporary', 'Unread') $script:UserProfileOtherSizeState = @('Unread', 'NotMeasured', 'CutShort', 'Complete', 'Missing', 'Unreadable') function Test-UserProfileOtherPerson { <# .SYNOPSIS Whether this User Profile belongs to a person, and not to the system, a service or a template. Pure. .DESCRIPTION By the number of the account, and by what Windows marks as special in Win32_UserProfile: the profile of the system, of the two service accounts, the default one and the public one. #> [CmdletBinding()] [OutputType([bool])] param([AllowNull()]$UserProfile) if ("$(Get-DataProperty $UserProfile 'Sid')".Trim() -notmatch $script:UserProfileOtherPersonSid) { return $false } -not [bool](Get-DataProperty $UserProfile 'Special') } function ConvertFrom-UserProfileOtherFileTime { <# .SYNOPSIS A time Windows keeps in the registry as two halves, as a local time; nothing where it kept none, which it says with zero. Pure. .DESCRIPTION A FILETIME, in a value ending in "High" and one ending in "Low". The registry hands each over as a signed number of 32 bits, so the lower half may be below zero. #> [CmdletBinding()] param([AllowNull()]$High, [AllowNull()]$Low) $upper = ConvertTo-Number $High $lower = ConvertTo-Number $Low if ($null -eq $upper -or $null -eq $lower) { return $null } $ticks = (([long]$upper -band 4294967295) -shl 32) -bor ([long]$lower -band 4294967295) if ($ticks -le 0) { return $null } try { [datetime]::FromFileTime($ticks) } catch { $null } } function ConvertTo-UserProfileOtherTime { <# .SYNOPSIS A time of a User Profile 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 Get-UserProfileOtherReading { <# .SYNOPSIS Every User Profile of a person on this machine, as Windows lists it. Decides nothing. .DESCRIPTION The two sources of Get-UserProfileReading, each asked once for all of them: the class Win32_UserProfile and the keys under ProfileList in the registry. One row for each account either knows, shaped as that function shapes it, and three things more: the name of the account, and the two times Windows keeps in the registry of when it loaded the User Profile and when it unloaded it. Those two are what "last used" is taken from, and not LastUseTime of Win32_UserProfile. That one is the time the file ntuser.dat was last written, and on current Windows a cumulative update and a virus scanner write it: every User Profile of a machine then reads as used on the day of the last update. The values LocalProfileLoadTime and LocalProfileUnloadTime (High and Low) are written by the profile service when somebody signs in and out, and by nothing else. They are readable without admin rights, for every account (seen on Windows 11). An account without admin rights is given no LastUseTime of another account at all. LastUseTime is kept in the row for where Windows has not written the two. The folder is the profile folder and nothing below it. It is in the row for the measuring and for telling a temporary User Profile, and goes no further: no row of this is kept as it is. See ConvertTo-UserProfileOtherEntry. #> [CmdletBinding()] [OutputType([psobject])] param() $listed = @{} try { Get-CimInstance -ClassName Win32_UserProfile -ErrorAction Stop | ForEach-Object { if ($_.SID) { $listed["$($_.SID)"] = $_ } } } catch { } $entries = @{} $backups = @{} try { Get-ChildItem -LiteralPath $script:UserProfileOtherListKey -ErrorAction Stop | ForEach-Object { $name = "$($_.PSChildName)" if ($name -like '*.bak') { $backups[$name.Substring(0, $name.Length - 4)] = $true; return } try { $entries[$name] = Get-ItemProperty -LiteralPath $_.PSPath -ErrorAction Stop } catch { } } } catch { } foreach ($sid in @(@($listed.Keys) + @($entries.Keys) + @($backups.Keys) | Sort-Object -Unique)) { $known = $listed[$sid] $entry = $entries[$sid] $path = "$(Get-DataProperty $known 'LocalPath')" if (-not $path) { $path = [Environment]::ExpandEnvironmentVariables("$(Get-DataProperty $entry 'ProfileImagePath')") } $row = [pscustomobject]@{ Sid = "$sid" Account = '' LocalPath = $path Status = Get-DataProperty $known 'Status' RoamingConfigured = Get-DataProperty $known 'RoamingConfigured' RoamingPath = Get-DataProperty $known 'RoamingPath' Special = Get-DataProperty $known 'Special' Loaded = Get-DataProperty $known 'Loaded' LastUseTime = Get-DataProperty $known 'LastUseTime' RegistryState = Get-DataProperty $entry 'State' RegistryBackupKey = [bool]$backups[$sid] CentralProfile = Get-DataProperty $entry 'CentralProfile' LoadedAt = ConvertFrom-UserProfileOtherFileTime -High (Get-DataProperty $entry 'LocalProfileLoadTimeHigh') -Low (Get-DataProperty $entry 'LocalProfileLoadTimeLow') UnloadedAt = ConvertFrom-UserProfileOtherFileTime -High (Get-DataProperty $entry 'LocalProfileUnloadTimeHigh') -Low (Get-DataProperty $entry 'LocalProfileUnloadTimeLow') } if (-not (Test-UserProfileOtherPerson -UserProfile $row)) { continue } # Asked of Windows once for each account in a Run, by the one function that asks: # see Resolve-AccountName. '' where Windows does not resolve the number - the # account was deleted, or its domain cannot be reached at the moment. $row.Account = (Resolve-AccountName -Sid $row.Sid).Name $row } } function ConvertTo-UserProfileOtherSize { <# .SYNOPSIS What is kept of the measuring of one profile folder: how it ended, by its name, how many bytes, and how many folders below it did not open. Pure. .DESCRIPTION Three properties, built here one by one, whatever the row carried: no path gets through, and no number of files, which is not what is reported of another User. Nothing measured is NotMeasured, and that is not zero bytes. #> [CmdletBinding()] [OutputType([psobject])] param([AllowNull()]$Row) $state = 'NotMeasured' $number = ConvertTo-Number (Get-DataProperty $Row 'State') if ($null -ne $number -and $number -ge 0 -and $number -lt $script:UserProfileMeasureState.Count) { $state = $script:UserProfileMeasureState[[int]$number] } [pscustomobject]@{ SizeState = $state SizeBytes = ConvertTo-UserProfileCount (Get-DataProperty $Row 'Bytes') SizeUnread = ConvertTo-UserProfileCount (Get-DataProperty $Row 'Unread') } } function ConvertTo-UserProfileOtherEntry { <# .SYNOPSIS What is kept of the User Profile of one person: the account and its number, the kind, a size and how it came about, when it was last used and where that is from, and whether it is loaded. Nothing for what belongs to no person. Pure. .DESCRIPTION ADR-0006 in code, for the User Profiles of other Users: the counterpart of ConvertTo-UserProfileFolder. Both Gatherers hand every row they read through here before it becomes data, and what the Elevated Part hands over goes through once more on arrival. Whatever a row carries on its way in, what goes on is built here, property by property: Sid, Account the number of the account and its name Kind out of a closed list, classified here and now, while what it is known by is still there: see Get-UserProfileKind SizeState out of a closed list; 'Unread' where nobody measured SizeBytes a number, and nothing where there is none SizeUnread how many folders below it did not open, as a number LastUsedAt one date: the later of the two times Windows keeps of loading and unloading the User Profile, and where it kept neither, the time its profile list gives LastUsedSource 'Registry' or 'Windows': which of those two it is Loaded yes, no, or nothing where that was not read There is no way through for where the User Profile lies, where it roams to, the flags Windows keeps of it, or the two times apart. A row that was through here already comes out as it went in: its kind and its date are taken as they are, and held to the same lists. .PARAMETER Size The measuring of the profile folder, as Measure-UserProfileFolder gives it. .PARAMETER Measured That the profile folders were measured: a row without a measuring is then one the time ran out before, and not one nobody measured. #> [CmdletBinding()] [OutputType([psobject])] param([AllowNull()]$Row, [AllowNull()]$Size, [switch]$Measured) if ($null -eq $Row -or -not (Test-UserProfileOtherPerson -UserProfile $Row)) { return } $has = { param([string]$Name) if ($Row -is [hashtable]) { return $Row.ContainsKey($Name) } [bool]$Row.PSObject.Properties[$Name] } $sid = "$(Get-DataProperty $Row 'Sid')".Trim() $account = "$(Get-DataProperty $Row 'Account')".Trim() $kind = 'Unread' if (& $has 'Kind') { $said = "$(Get-DataProperty $Row 'Kind')" $known = $script:UserProfileOtherKind | Where-Object { $_ -ceq $said } | Select-Object -First 1 if ($known) { $kind = $known } } elseif ($null -ne (Get-DataProperty $Row 'Status') -or $null -ne (Get-DataProperty $Row 'RegistryState') -or "$(Get-DataProperty $Row 'LocalPath')".Trim()) { $kind = (Get-UserProfileKind -UserProfile $Row -Account $account).Kind } $lastUsed = $null $source = $null if (& $has 'LastUsedAt') { $lastUsed = ConvertTo-UserProfileOtherTime (Get-DataProperty $Row 'LastUsedAt') if ($null -ne $lastUsed) { $source = $(if ("$(Get-DataProperty $Row 'LastUsedSource')" -ceq 'Windows') { 'Windows' } else { 'Registry' }) } } else { $kept = @(@((Get-DataProperty $Row 'LoadedAt'), (Get-DataProperty $Row 'UnloadedAt')) | ForEach-Object { ConvertTo-UserProfileOtherTime $_ } | Where-Object { $null -ne $_ } | Sort-Object) if ($kept.Count) { $lastUsed = $kept[-1]; $source = 'Registry' } else { $lastUsed = ConvertTo-UserProfileOtherTime (Get-DataProperty $Row 'LastUseTime') if ($null -ne $lastUsed) { $source = 'Windows' } } } $state = 'Unread'; $bytes = $null; $unopened = [long]0 if ($Measured -or $null -ne $Size) { $measuring = ConvertTo-UserProfileOtherSize -Row $Size $state = $measuring.SizeState; $bytes = $measuring.SizeBytes; $unopened = $measuring.SizeUnread } elseif (& $has 'SizeState') { $said = "$(Get-DataProperty $Row 'SizeState')" $known = $script:UserProfileOtherSizeState | Where-Object { $_ -ceq $said } | Select-Object -First 1 if ($known -and $known -ne 'Unread') { $state = $known $bytes = ConvertTo-UserProfileCount (Get-DataProperty $Row 'SizeBytes') $unopened = ConvertTo-UserProfileCount (Get-DataProperty $Row 'SizeUnread') } } $loaded = Get-DataProperty $Row 'Loaded' if ($null -ne $loaded) { $loaded = [bool]$loaded } [pscustomobject]@{ Sid = $sid Account = $account Kind = $kind SizeState = $state SizeBytes = $bytes SizeUnread = $unopened LastUsedAt = $lastUsed LastUsedSource = $source Loaded = $loaded } } function Get-UserProfileOtherData { <# .SYNOPSIS The User Profiles of the machine as this account reads them, and beside them what the Run knows already: the same read with admin rights, and what the Storage Check gathered. Decides nothing. .DESCRIPTION Of each User Profile what a Report may hold of another User and no more: both lists go through ConvertTo-UserProfileOtherEntry here, the one read with admin rights for the second time. Which of the reading with admin rights fills in what, and whether the system drive is short of space, is for the pure half. .PARAMETER Observed 'UserProfileElevated' is absent where that Check was not performed: nobody read with admin rights in this Run. 'Storage' is absent where the Storage Check was not performed. #> [CmdletBinding()] [OutputType([psobject])] param([AllowNull()][hashtable]$Observed = @{}) $elevated = $null if ($Observed -and $Observed.ContainsKey('UserProfileElevated')) { $elevated = $Observed['UserProfileElevated'] } $storage = $null if ($Observed -and $Observed.ContainsKey('Storage')) { $storage = $Observed['Storage'] } [pscustomobject]@{ Listed = @(Get-UserProfileOtherReading | ForEach-Object { ConvertTo-UserProfileOtherEntry -Row $_ }) ElevatedRead = ($null -ne $elevated) ElevatedProfiles = @((Get-DataCollection $elevated 'Profiles') | ForEach-Object { $_ } | ForEach-Object { ConvertTo-UserProfileOtherEntry -Row $_ }) OtherMeasureSeconds = Get-DataProperty $elevated 'OtherMeasureSeconds' Storage = $storage } } function ConvertTo-UserProfileOtherRow { <# .SYNOPSIS Every User Profile of a person on the machine, with what this account could not read of it filled in from the reading taken with admin rights. Pure. .DESCRIPTION Matched on the number of the account, which is what a User Profile is kept under. What this account read itself stays; only what it left unread is filled in. A User Profile only the reading with admin rights has is added: it is on the disk whoever saw it. The size of the own User Profile is the one its Check measured, folder by folder. Of every other it is the one the reading with admin rights gives. SizeState says how the number came about: Complete measured to the end CutShort the time was up: a lower bound Partial measured to the end, with folders below it that did not open: a lower bound NotMeasured the time was up before this one's turn: no number Unreadable the profile folder did not open, with admin rights either: no number Missing there is no profile folder: no number Unread nobody measured it: no number LastUsedAt is the later of the two times Windows keeps of loading and unloading the User Profile, and LastUsedSource then 'Registry'. Where it kept neither it is the time the profile list of Windows gives, and LastUsedSource 'Windows': see Get-UserProfileOtherReading for why that one comes second. One of the registry comes before one of the profile list, whichever reading has it. The kind is the one this account read. Where it read none, or found nothing that marks the User Profile as other than local and the reading with admin rights did, it is that one's: an administrator is given more of the marks. The rows it works from hold what a Report may hold of another User and no more: see ConvertTo-UserProfileOtherEntry, which every row is handed through once more here, so that data from anywhere is held to it. A row is built property by property, and whatever else it carried is not carried on. Not filled in by Join-ElevatedReading, by which the Program and the Session Kind fill in. That one keeps to the rows of the Main Part and takes from the other reading what this one left absent, and three things here are another rule: a User Profile only the other reading has is added; a kind read as local is not absent and is replaced all the same; and which date of last use counts goes by where it is from and not by who read it. Why a size is still unread is asked the same way as there: Get-UnreadReason. #> [CmdletBinding()] [OutputType([psobject])] param([AllowNull()]$Data) $other = Get-DataProperty $Data 'Other' if ($null -eq $other) { return } $ownSid = "$(Get-DataProperty $Data 'OwnSid')".Trim() $own = Get-UserProfileOwn -Data $Data $mine = @{} $theirs = @{} $sids = New-Object System.Collections.Generic.List[string] (Get-DataCollection $other 'Listed') | ForEach-Object { $_ } | ForEach-Object { ConvertTo-UserProfileOtherEntry -Row $_ } | ForEach-Object { if (-not $mine.ContainsKey($_.Sid)) { $mine[$_.Sid] = $_; $sids.Add($_.Sid) } } if ($null -ne $own -and $ownSid -and -not $mine.ContainsKey($ownSid)) { # The own one, where the listing does not have it: as its own Check read it. $entry = ConvertTo-UserProfileOtherEntry -Row $own if ($null -ne $entry) { $mine[$ownSid] = $entry; $sids.Add($ownSid) } } (Get-DataCollection $other 'ElevatedProfiles') | ForEach-Object { $_ } | ForEach-Object { ConvertTo-UserProfileOtherEntry -Row $_ } | ForEach-Object { if ($theirs.ContainsKey($_.Sid)) { return } $theirs[$_.Sid] = $_ if (-not $mine.ContainsKey($_.Sid)) { $sids.Add($_.Sid) } } $ownSize = $null if ($null -ne $own) { $ownSize = Get-UserProfileSize -UserProfile $own } $rows = @(foreach ($sid in $sids) { $here = $mine[$sid] $there = $theirs[$sid] # What this account read, and where it read nothing, what was read with admin rights. $account = "$(Get-DataProperty $here 'Account')".Trim() if (-not $account) { $account = "$(Get-DataProperty $there 'Account')".Trim() } $kind = "$(Get-DataProperty $here 'Kind')" $otherKind = "$(Get-DataProperty $there 'Kind')" if ($otherKind -and $otherKind -ne 'Unread' -and $kind -in '', 'Unread', 'Local') { $kind = $otherKind } if (-not $kind) { $kind = 'Unread' } $lastUsed = $null $source = $null $dated = @(@($here, $there) | Where-Object { $null -ne (Get-DataProperty $_ 'LastUsedAt') }) $kept = @($dated | Where-Object { "$(Get-DataProperty $_ 'LastUsedSource')" -eq 'Registry' } | ForEach-Object { Get-DataProperty $_ 'LastUsedAt' } | Sort-Object) if ($kept.Count) { $lastUsed = $kept[-1]; $source = 'Registry' } elseif ($dated.Count) { $lastUsed = Get-DataProperty $dated[0] 'LastUsedAt'; $source = 'Windows' } $loaded = Get-DataProperty $here 'Loaded' if ($null -eq $loaded) { $loaded = Get-DataProperty $there 'Loaded' } $bytes = $null $state = 'Unread' if ($sid -eq $ownSid -and $null -ne $ownSize -and $ownSize.Counted) { $bytes = [long]$ownSize.TotalBytes $state = $(if (-not $ownSize.TotalAtLeast) { 'Complete' } elseif ($ownSize.CutShortInside) { 'CutShort' } else { 'Partial' }) } elseif ($null -ne $there) { $measured = ConvertTo-Number (Get-DataProperty $there 'SizeBytes') $unopened = ConvertTo-Number (Get-DataProperty $there 'SizeUnread') switch ("$(Get-DataProperty $there 'SizeState')") { 'Complete' { if ($null -ne $measured) { $bytes = [long]$measured; $state = $(if ($unopened -gt 0) { 'Partial' } else { 'Complete' }) } } 'CutShort' { if ($null -ne $measured) { $bytes = [long]$measured; $state = 'CutShort' } } 'NotMeasured' { $state = 'NotMeasured' } 'Unreadable' { $state = 'Unreadable' } 'Missing' { $state = 'Missing' } } } [pscustomobject]@{ Account = $(if ($account) { $account } else { $null }) Sid = $sid Kind = $kind SizeBytes = $bytes SizeState = $state LastUsedAt = $lastUsed LastUsedSource = $source Loaded = [bool]$loaded Own = [bool]($ownSid -and $sid -eq $ownSid) } }) # Three keys, so that the order does not depend on how a sort treats equals, nor on # the order Windows listed the User Profiles in. @($rows | Sort-Object @{ Expression = { if ($null -ne $_.SizeBytes) { $_.SizeBytes } else { [long]-1 } }; Descending = $true }, @{ Expression = { "$($_.Account)".ToLowerInvariant() }; Descending = $false }, @{ Expression = 'Sid'; Descending = $false }) } function Get-UserProfileOtherAccountText { <# .SYNOPSIS What a User Profile is called in the Report: its account, and where Windows did not resolve one, that in words with the number beside it. Pure. .DESCRIPTION The number never stands alone. It is shown, because it is what the entry of the User Profile in the registry is found by. #> [CmdletBinding()] [OutputType([string])] param([AllowNull()]$Row) $account = "$(Get-DataProperty $Row 'Account')".Trim() if ($account) { return $account } (Get-Text 'Value.UserProfile.Other.AccountUnknown') -f "$(Get-DataProperty $Row 'Sid')" } function Get-UserProfileOtherFinding { <# .SYNOPSIS How many User Profiles of the machine nobody has used for a long time, and how much room they take together. Pure. .DESCRIPTION From how many days on a User Profile counts as unused is the Judge's limit, which hands it over: UnusedDays, see ConvertTo-UserProfileFinding. The own User Profile and one that is loaded now never count, whatever their times say. One whose last use is not known does not count either: it is said how many those are. Information, and a warning only where the system drive is short of space. Whether it is, is the Storage Check's to say and is asked of it: Get-StorageFreeSpace, with what that Check gathered and the limits of its Check Definition, which it hands on with what it gathered. There is no second definition of "short" here. Where the Storage Check was not performed or could not read the drive, this stays information and says that free space was not weighed. Nothing where the User Profiles of the machine were not listed at all: data from before this was read. .PARAMETER UnusedDays After how many days without use a User Profile counts as unused. #> [CmdletBinding()] [OutputType([psobject])] param([AllowNull()]$Data, [double]$UnusedDays = 180) $other = Get-DataProperty $Data 'Other' if ($null -eq $other) { return } $check = Get-Text 'Check.UserProfile.Other.Unused' $rows = @(ConvertTo-UserProfileOtherRow -Data $Data) if (-not $rows.Count) { return New-UnavailableFinding -Category User -Check $check -Hint (Get-Text 'Hint.UserProfile.Repeat') } $gatheredAt = ConvertTo-UserProfileOtherTime (Get-DataProperty $Data 'GatheredAt') $candidates = @($rows | Where-Object { -not $_.Own -and -not $_.Loaded }) $unknown = @($candidates | Where-Object { $null -eq $_.LastUsedAt -or $null -eq $gatheredAt }) $unused = @($candidates | Where-Object { $null -ne $_.LastUsedAt -and $null -ne $gatheredAt -and ($gatheredAt - $_.LastUsedAt).TotalDays -gt $UnusedDays }) $value = @((Get-Text 'Value.UserProfile.Other.Count') -f $rows.Count) $reference = @(New-Reference -Section 'Title.UserProfile.Other') if (-not $unused.Count) { $value += (Get-Text 'Value.UserProfile.Other.NoneUnused') -f $UnusedDays if (-not $unknown.Count) { return New-Finding -Category User -Check $check -Severity OK -Value ($value -join ' | ') -Reference $reference } # Not "none": of these it is not known. $value += (Get-Text 'Value.UserProfile.Other.LastUseUnknown') -f $unknown.Count return New-Finding -Category User -Check $check -Severity INFO -Value ($value -join ' | ') -Reference $reference ` -Meaning (Get-Text 'Meaning.UserProfile.Other.LastUseUnknown') -Hint (Get-Text 'Hint.UserProfile.Repeat') } $names = @($unused | ForEach-Object { Get-UserProfileOtherAccountText -Row $_ } | Sort-Object) $value += (Get-Text 'Value.UserProfile.Other.Unused') -f $unused.Count, $UnusedDays, ($names -join ', ') # A profile folder that is not there takes no room, and that is known. $sized = @($unused | Where-Object { $null -ne $_.SizeBytes }) $open = @($unused | Where-Object { $_.SizeState -notin 'Complete', 'Missing' }) $total = [long]0 foreach ($row in $sized) { $total += [long]$row.SizeBytes } if (-not $sized.Count -and $open.Count) { $value += Get-Text 'Value.UserProfile.Other.UnusedSizeUnknown' } elseif ($open.Count) { $value += (Get-Text 'Value.UserProfile.Other.UnusedSizeAtLeast') -f (Format-DataSize -Bytes $total), $open.Count } else { $value += (Get-Text 'Value.UserProfile.Other.UnusedSize') -f (Format-DataSize -Bytes $total) } # Whose fault it is that a size is not there: nobody measured with admin rights in # this Run, or the measuring with admin rights did not get it. if (@($open | Where-Object { $_.SizeState -ne 'Partial' }).Count) { if ((Get-UnreadReason $other) -eq 'Refused') { $value += Get-Text 'Value.UserProfile.Other.SizeProtected' } else { $value += Get-Text 'Value.UserProfile.Other.SizeNoAdmin' } } if ($unknown.Count) { $value += (Get-Text 'Value.UserProfile.Other.LastUseUnknown') -f $unknown.Count } $meaning = (Get-Text 'Meaning.UserProfile.Other.Unused') -f $UnusedDays $remove = Get-Text 'Hint.UserProfile.Other.Remove' $space = Get-StorageFreeSpace -Data (Get-DataProperty $other 'Storage') if ($null -eq $space) { $value += Get-Text 'Value.UserProfile.Other.FreeNotWeighed' return New-Finding -Category User -Check $check -Severity INFO -Value ($value -join ' | ') -Reference $reference ` -Meaning ($meaning + ' ' + (Get-Text 'Meaning.UserProfile.Other.NotWeighed')) ` -Hint ((Get-Text 'Hint.UserProfile.Other.NoRush') + ' | ' + $remove) } if ($space.Severity -ne 'OK') { $value += (Get-Text 'Value.UserProfile.Other.FreeLow') -f $space.Drive, $space.FreeGB, $space.FreePercent return New-Finding -Category User -Check $check -Severity WARN -Value ($value -join ' | ') ` -Reference @($reference; (New-Reference -Signal 'system-drive-low')) ` -Meaning ($meaning + ' ' + (Get-Text 'Meaning.UserProfile.Other.Low')) -Hint $remove } $value += (Get-Text 'Value.UserProfile.Other.FreeFine') -f $space.Drive, $space.FreeGB, $space.FreePercent New-Finding -Category User -Check $check -Severity INFO -Value ($value -join ' | ') -Reference $reference ` -Meaning $meaning -Hint ((Get-Text 'Hint.UserProfile.Other.NoRush') + ' | ' + $remove) } function ConvertTo-UserProfileOtherSection { <# .SYNOPSIS Every User Profile of a person on the machine as a table, the own one included. Pure. .DESCRIPTION True of the machine, and shown whichever Session the Run is in. The own one is the User Profile of the account the Run runs as: the User's where the Run is in the Session of the User, and said not to be where it is not. No cell is empty. What is not there is said in one of two wordings, and the last column says the same at length: "nicht gelesen" where nobody read with admin rights in this Run, "nicht lesbar" where Windows did not give it with admin rights either. See Get-UnreadText. Nothing where the User Profiles of the machine were not listed. #> [CmdletBinding()] [OutputType([psobject])] param([AllowNull()]$Data) $other = Get-DataProperty $Data 'Other' if ($null -eq $other) { return } $rows = @(ConvertTo-UserProfileOtherRow -Data $Data) if (-not $rows.Count) { return } $elevated = (Get-UnreadReason $other) -eq 'Refused' $wrong = [bool](Get-DataProperty (Get-DataProperty $Data 'RunUser') 'Differs') $seconds = ConvertTo-Number (Get-DataProperty $other 'OtherMeasureSeconds') if ($null -eq $seconds) { $seconds = 60 } $unread = Get-UnreadText -Reason (Get-UnreadReason $other) New-Section -Title (Get-Text 'Title.UserProfile.Other') -Row @( foreach ($entry in $rows) { $note = @() if ($entry.Own -and $wrong) { $note += Get-Text 'Value.UserProfile.Other.Note.OwnRun' } elseif ($entry.Own) { $note += Get-Text 'Value.UserProfile.Other.Note.Own' } $sizeCell = $unread switch ($entry.SizeState) { 'Complete' { $sizeCell = Format-DataSize -Bytes $entry.SizeBytes } 'CutShort' { $sizeCell = Format-DataSize -Bytes $entry.SizeBytes -AtLeast # Of the own one the time was that of its own Check, and its Finding says so. if ($entry.Own) { $note += Get-Text 'Value.UserProfile.Note.CutShort' } else { $note += (Get-Text 'Value.UserProfile.Other.Note.SizeCutShort') -f $seconds } } 'Partial' { $sizeCell = Format-DataSize -Bytes $entry.SizeBytes -AtLeast $note += Get-Text 'Value.UserProfile.Other.Note.SizePartial' } 'NotMeasured' { $sizeCell = Get-Text 'Value.UserProfile.Cell.NotMeasured'; $note += (Get-Text 'Value.UserProfile.Other.Note.SizeNotMeasured') -f $seconds } 'Missing' { $sizeCell = Get-Text 'Value.UserProfile.Other.Cell.Missing'; $note += Get-Text 'Value.UserProfile.Other.Note.Missing' } default { if ($elevated) { $note += Get-Text 'Value.UserProfile.Other.Note.SizeProtected' } else { $note += Get-Text 'Value.UserProfile.Other.Note.SizeUnread' } } } $usedCell = $unread if ($entry.Loaded) { $usedCell = Get-Text 'Value.UserProfile.Other.Cell.Loaded' } elseif ($null -ne $entry.LastUsedAt) { $usedCell = (Get-Text 'Value.UserProfile.Other.At') -f $entry.LastUsedAt if ($entry.LastUsedSource -eq 'Windows') { $note += Get-Text 'Value.UserProfile.Other.Note.LastUseWindows' } } elseif ($elevated) { $usedCell = Get-Text 'Value.UserProfile.Other.Cell.NotKept'; $note += Get-Text 'Value.UserProfile.Other.Note.LastUseProtected' } else { $note += Get-Text 'Value.UserProfile.Other.Note.LastUseUnread' } $row = [ordered]@{} $row[(Get-Text 'Column.UserProfile.Other.Account')] = Get-UserProfileOtherAccountText -Row $entry # A kind neither reading has is unread for the reason every cell here is. $row[(Get-Text 'Column.UserProfile.Other.Kind')] = $(if ($entry.Kind -eq 'Unread') { $unread } else { Get-UserProfileKindText -Kind $entry.Kind }) $row[(Get-Text 'Column.UserProfile.Other.Size')] = $sizeCell $row[(Get-Text 'Column.UserProfile.Other.LastUsed')] = $usedCell $row[(Get-Text 'Column.Note')] = $note -join '; ' [pscustomobject]$row } ) } |