Files
management-scripts/Get-UserLogonActivity.ps1
T

584 lines
18 KiB
PowerShell
Raw Normal View History

2026-07-07 14:45:18 -04:00
#Requires -Version 5.1
<#
.SYNOPSIS
2026-07-07 15:39:57 -04:00
Gets user logon, logoff, lock, unlock, and power activity from Windows event logs.
2026-07-07 14:45:18 -04:00
.EXAMPLE
$Start = '2026-07-03T00:00:00Z'
$End = '2026-07-06T00:00:00Z'
.\Get-UserLogonActivity.ps1
.EXAMPLE
.\Get-UserLogonActivity.ps1
2026-07-07 14:45:18 -04:00
#>
$StartVariable = Get-Variable -Name Start -ErrorAction SilentlyContinue
$EndVariable = Get-Variable -Name End -ErrorAction SilentlyContinue
$UseDateRange = ($null -ne $StartVariable -and $null -ne $EndVariable)
2026-07-07 14:45:18 -04:00
If (($null -ne $StartVariable) -xor ($null -ne $EndVariable)) {
Write-Error '$Start and $End must either both be defined or both be omitted. If omitted, the script returns the latest 15 activity events.'
2026-07-07 14:45:18 -04:00
Exit 1
}
Function ConvertTo-ActivityDateTime {
Param(
[Parameter(Mandatory=$true)]
[AllowNull()]
[object]$Value,
[Parameter(Mandatory=$true)]
[string]$ParameterName
)
If ($Value -is [DateTimeOffset]) {
Return $Value
}
If ($Value -is [DateTime]) {
Return [DateTimeOffset]$Value
}
$TrimmedValue = ([string]$Value).Trim()
$Culture = [Globalization.CultureInfo]::InvariantCulture
$ParsedDateTime = [DateTimeOffset]::MinValue
$UtcFormats = [string[]]@(
"yyyy-MM-dd'T'HH:mm:ss'Z'",
"yyyy-MM-dd'T'HH:mm:ss.FFFFFFF'Z'"
)
$OffsetFormats = [string[]]@(
"yyyy-MM-dd'T'HH:mm:sszzz",
"yyyy-MM-dd'T'HH:mm:ss.FFFFFFFzzz"
)
$LocalFormats = [string[]]@(
"yyyy-MM-dd'T'HH:mm:ss",
"yyyy-MM-dd'T'HH:mm:ss.FFFFFFF",
"yyyy-MM-dd HH:mm:ss",
"yyyy-MM-dd",
"MM/dd/yyyy HH:mm:ss",
"MM/dd/yyyy"
)
$UtcStyles = [Globalization.DateTimeStyles]::AllowWhiteSpaces -bor [Globalization.DateTimeStyles]::AssumeUniversal -bor [Globalization.DateTimeStyles]::AdjustToUniversal
$LocalStyles = [Globalization.DateTimeStyles]::AllowWhiteSpaces -bor [Globalization.DateTimeStyles]::AssumeLocal
If ([DateTimeOffset]::TryParseExact($TrimmedValue, $UtcFormats, $Culture, $UtcStyles, [ref]$ParsedDateTime)) {
Return $ParsedDateTime
}
If ([DateTimeOffset]::TryParseExact($TrimmedValue, $OffsetFormats, $Culture, [Globalization.DateTimeStyles]::AllowWhiteSpaces, [ref]$ParsedDateTime)) {
Return $ParsedDateTime
}
If ([DateTimeOffset]::TryParseExact($TrimmedValue, $LocalFormats, $Culture, $LocalStyles, [ref]$ParsedDateTime)) {
Return $ParsedDateTime
}
If ([DateTimeOffset]::TryParse($TrimmedValue, $Culture, $LocalStyles, [ref]$ParsedDateTime)) {
Return $ParsedDateTime
}
Throw "Invalid -${ParameterName} datetime '${Value}'. Use ISO 8601 UTC format: yyyy-MM-ddTHH:mm:ssZ. Example: 2026-07-03T00:00:00Z"
}
Function Get-EventDataValue {
Param(
[Parameter(Mandatory=$true)]
[xml]$EventXml,
[Parameter(Mandatory=$true)]
[string]$Name
)
$Data = $EventXml.Event.EventData.Data | Where-Object { $_.Name -eq $Name } | Select-Object -First 1
If ($null -eq $Data) { Return $null }
Return [string]$Data.'#text'
}
Function Get-EventDataValueFromList {
Param(
[Parameter(Mandatory=$true)]
[xml]$EventXml,
[Parameter(Mandatory=$true)]
[string[]]$Names
)
ForEach ($Name in $Names) {
$Value = Get-EventDataValue -EventXml $EventXml -Name $Name
If (-not [string]::IsNullOrWhiteSpace($Value) -and $Value -ne '-') {
Return $Value
}
}
Return $null
}
2026-07-07 14:45:18 -04:00
Function Test-UserLogonActivityUser {
Param(
[AllowNull()]
[string]$UserName
)
If ([string]::IsNullOrWhiteSpace($UserName)) { Return $false }
2026-07-07 15:23:15 -04:00
$LeafUserName = ($UserName -split '\\')[-1]
If ($LeafUserName -eq '-') { Return $false }
If ($LeafUserName.EndsWith('$')) { Return $false }
If ($LeafUserName -in @('ANONYMOUS LOGON', 'LOCAL SERVICE', 'NETWORK SERVICE', 'SYSTEM')) { Return $false }
If ($LeafUserName -match '^(DWM|UMFD)-\d+$') { Return $false }
2026-07-07 14:45:18 -04:00
Return $true
}
Function Format-UserName {
Param(
[AllowNull()]
[string]$DomainName,
[Parameter(Mandatory=$true)]
[string]$UserName
)
If ([string]::IsNullOrWhiteSpace($DomainName) -or $DomainName -eq '-') {
Return $UserName
}
Return "${DomainName}\${UserName}"
}
2026-07-07 15:23:15 -04:00
Function Resolve-UserLogonActivitySid {
Param(
[AllowNull()]
[object]$Sid
)
If ($null -eq $Sid) { Return $null }
$SidString = ([string]$Sid).Trim()
If ([string]::IsNullOrWhiteSpace($SidString) -or $SidString -eq '-') { Return $null }
Try {
2026-07-07 15:23:15 -04:00
Return (New-Object Security.Principal.SecurityIdentifier($SidString)).Translate([Security.Principal.NTAccount]).Value
}
Catch {
2026-07-07 15:23:15 -04:00
$ProfilePath = (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\${SidString}" -Name ProfileImagePath -ErrorAction SilentlyContinue).ProfileImagePath
If (-not [string]::IsNullOrWhiteSpace($ProfilePath)) {
Return (Split-Path -Path $ProfilePath -Leaf)
}
Return $SidString
}
}
Function Get-UserLogonActivitySidFromEvent {
Param(
[Parameter(Mandatory=$true)]
[object]$Event
)
$EventXml = [xml]$Event.ToXml()
$UserSid = Get-EventDataValueFromList -EventXml $EventXml -Names @('UserSid', 'TargetUserSid', 'SubjectUserSid')
If (-not [string]::IsNullOrWhiteSpace($UserSid)) {
Return $UserSid
}
ForEach ($Property in $Event.Properties) {
$PropertyValue = [string]$Property.Value
If ($PropertyValue -match '^S-\d-\d+-.+') {
Return $PropertyValue
}
}
Return [string]$EventXml.Event.System.Security.UserID
}
Function New-UserLogonActivityRecord {
Param(
[Parameter(Mandatory=$true)]
[datetime]$TimeCreated,
[Parameter(Mandatory=$true)]
[string]$User,
[Parameter(Mandatory=$true)]
2026-07-07 15:39:57 -04:00
[ValidateSet('logon', 'logoff', 'unlock', 'lock', 'sleep', 'wake', 'shutdown', 'restart', 'power on')]
[string]$Activity,
2026-07-07 17:30:11 -04:00
[Parameter(Mandatory=$true)]
[string]$Source,
[int]$SourcePriority = 50,
2026-07-07 15:39:57 -04:00
[switch]$AllowSystemUser
2026-07-07 15:23:15 -04:00
)
2026-07-07 15:39:57 -04:00
If (-not $AllowSystemUser -and -not (Test-UserLogonActivityUser -UserName $User)) { Return $null }
2026-07-07 15:23:15 -04:00
[PSCustomObject]@{
Date = $TimeCreated.ToString('yyyy-MM-dd')
Time = $TimeCreated.ToString('HH:mm:ss')
User = $User
Activity = $Activity
2026-07-07 17:30:11 -04:00
Source = $Source
SourcePriority = $SourcePriority
2026-07-07 15:23:15 -04:00
TimeCreated = $TimeCreated
}
}
Function ConvertTo-WinlogonUserActivity {
Param(
[Parameter(Mandatory=$true)]
[object]$Event
)
Switch ($Event.Id) {
2026-07-07 17:30:11 -04:00
7001 { $Activity = 'logon'; $Source = 'Winlogon 7001' }
7002 { $Activity = 'logoff'; $Source = 'Winlogon 7002' }
2026-07-07 15:23:15 -04:00
Default { Return $null }
}
2026-07-07 15:23:15 -04:00
$User = Resolve-UserLogonActivitySid -Sid (Get-UserLogonActivitySidFromEvent -Event $Event)
If ([string]::IsNullOrWhiteSpace($User)) { Return $null }
2026-07-07 17:30:11 -04:00
New-UserLogonActivityRecord -TimeCreated $Event.TimeCreated -User $User -Activity $Activity -Source $Source -SourcePriority 10
}
2026-07-07 15:23:15 -04:00
Function ConvertTo-SecurityUserActivity {
2026-07-07 14:45:18 -04:00
Param(
[Parameter(Mandatory=$true)]
[object]$Event
)
$EventXml = [xml]$Event.ToXml()
$UserName = Get-EventDataValueFromList -EventXml $EventXml -Names @('TargetUserName', 'SubjectUserName')
$DomainName = Get-EventDataValueFromList -EventXml $EventXml -Names @('TargetDomainName', 'SubjectDomainName')
2026-07-07 17:30:11 -04:00
$LogonType = Get-EventDataValue -EventXml $EventXml -Name 'LogonType'
2026-07-07 14:45:18 -04:00
$Activity = $null
2026-07-07 17:30:11 -04:00
$Source = $null
$SourcePriority = 20
2026-07-07 14:45:18 -04:00
Switch ($Event.Id) {
2026-07-07 17:30:11 -04:00
4624 {
If ($LogonType -ne '7') { Return $null }
$Activity = 'unlock'
$Source = 'Security 4624 type 7'
$SourcePriority = 30
}
2026-07-07 14:45:18 -04:00
4800 {
$Activity = 'lock'
2026-07-07 17:30:11 -04:00
$Source = 'Security 4800'
2026-07-07 14:45:18 -04:00
}
4801 {
$Activity = 'unlock'
2026-07-07 17:30:11 -04:00
$Source = 'Security 4801'
2026-07-07 14:45:18 -04:00
}
Default {
Return $null
}
}
2026-07-07 17:30:11 -04:00
New-UserLogonActivityRecord -TimeCreated $Event.TimeCreated -User (Format-UserName -DomainName $DomainName -UserName $UserName) -Activity $Activity -Source $Source -SourcePriority $SourcePriority
2026-07-07 15:23:15 -04:00
}
2026-07-07 15:39:57 -04:00
Function ConvertTo-PowerUserActivity {
Param(
[Parameter(Mandatory=$true)]
[object]$Event
)
$EventXml = [xml]$Event.ToXml()
$ProviderName = [string]$Event.ProviderName
$User = 'SYSTEM'
$Activity = $null
If ($ProviderName -ieq 'Microsoft-Windows-Kernel-Power' -and $Event.Id -eq 42) {
$Activity = 'sleep'
2026-07-07 17:30:11 -04:00
$Source = 'Kernel-Power 42'
2026-07-07 15:39:57 -04:00
}
ElseIf ($ProviderName -ieq 'Microsoft-Windows-Power-Troubleshooter' -and $Event.Id -eq 1) {
$Activity = 'wake'
2026-07-07 17:30:11 -04:00
$Source = 'Power-Troubleshooter 1'
2026-07-07 15:39:57 -04:00
}
ElseIf ($ProviderName -ieq 'EventLog' -and $Event.Id -eq 6005) {
$Activity = 'power on'
2026-07-07 17:30:11 -04:00
$Source = 'EventLog 6005'
2026-07-07 15:39:57 -04:00
}
ElseIf ($ProviderName -ieq 'EventLog' -and $Event.Id -eq 6006) {
$Activity = 'shutdown'
2026-07-07 17:30:11 -04:00
$Source = 'EventLog 6006'
2026-07-07 15:39:57 -04:00
}
ElseIf ($ProviderName -ieq 'User32' -and $Event.Id -eq 1074) {
$ShutdownType = Get-EventDataValue -EventXml $EventXml -Name 'param5'
$EventUser = Get-EventDataValue -EventXml $EventXml -Name 'param7'
If (-not [string]::IsNullOrWhiteSpace($EventUser) -and $EventUser -ne '-') {
$User = $EventUser
}
If ($ShutdownType -match 'restart') {
$Activity = 'restart'
}
Else {
$Activity = 'shutdown'
}
2026-07-07 17:30:11 -04:00
$Source = 'User32 1074'
2026-07-07 15:39:57 -04:00
}
Else {
Return $null
}
2026-07-07 17:30:11 -04:00
New-UserLogonActivityRecord -TimeCreated $Event.TimeCreated -User $User -Activity $Activity -Source $Source -SourcePriority 10 -AllowSystemUser
2026-07-07 15:39:57 -04:00
}
2026-07-07 15:23:15 -04:00
Function Get-UserLogonActivityWinEvent {
Param(
[Parameter(Mandatory=$true)]
[hashtable]$FilterHashtable,
2026-07-07 14:45:18 -04:00
2026-07-07 15:23:15 -04:00
[int]$MaxEvents = 0,
2026-07-07 14:45:18 -04:00
2026-07-07 15:23:15 -04:00
[Parameter(Mandatory=$true)]
[string]$LogDescription
)
$Parameters = @{
FilterHashtable = $FilterHashtable
ErrorAction = 'Stop'
}
If ($MaxEvents -gt 0) {
$Parameters.MaxEvents = $MaxEvents
}
Try {
Return @(Get-WinEvent @Parameters)
}
Catch {
If ($_.FullyQualifiedErrorId -like '*NoMatchingEventsFound*' -or $_.Exception.Message -like '*No events were found*') {
Return @()
}
Write-Error "Unable to read ${LogDescription}. Run PowerShell as administrator if access is denied. $($_.Exception.Message)"
Return @()
2026-07-07 14:45:18 -04:00
}
}
2026-07-07 15:23:15 -04:00
Function Add-UserLogonActivityRecord {
Param(
[Parameter(Mandatory=$true)]
[object]$ActivityEvents,
[AllowNull()]
[object]$ActivityEvent
)
If ($null -ne $ActivityEvent) {
$ActivityEvents.Add($ActivityEvent) | Out-Null
}
}
2026-07-07 15:23:15 -04:00
Function Get-UniqueUserLogonActivity {
Param(
[AllowNull()]
[object[]]$ActivityEvents
)
$UniqueEvents = New-Object System.Collections.Generic.List[object]
2026-07-07 17:30:11 -04:00
ForEach ($ActivityEvent in (@($ActivityEvents) | Where-Object { $null -ne $_ } | Sort-Object -Property TimeCreated,User,Activity,SourcePriority)) {
2026-07-07 15:23:15 -04:00
$IsDuplicate = $false
2026-07-07 17:30:11 -04:00
For ($Index = 0; $Index -lt $UniqueEvents.Count; $Index++) {
$UniqueEvent = $UniqueEvents[$Index]
2026-07-07 15:23:15 -04:00
If (
$UniqueEvent.User -eq $ActivityEvent.User -and
$UniqueEvent.Activity -eq $ActivityEvent.Activity -and
[Math]::Abs(($UniqueEvent.TimeCreated - $ActivityEvent.TimeCreated).TotalSeconds) -le 5
) {
2026-07-07 17:30:11 -04:00
If ($ActivityEvent.SourcePriority -lt $UniqueEvent.SourcePriority) {
$UniqueEvents[$Index] = $ActivityEvent
}
2026-07-07 15:23:15 -04:00
$IsDuplicate = $true
Break
}
}
If (-not $IsDuplicate) {
$UniqueEvents.Add($ActivityEvent) | Out-Null
}
}
Return $UniqueEvents
}
Function Write-UserLogonActivityOutput {
Param(
[AllowNull()]
[object[]]$ActivityEvents
)
2026-07-07 17:30:11 -04:00
$Rows = @($ActivityEvents | Select-Object Date,Time,User,Activity,Source)
2026-07-07 15:23:15 -04:00
If ($Rows.Count -eq 0) {
Write-Output 'No matching user logon activity events found.'
Return
}
Write-Output (($Rows | Format-Table -AutoSize | Out-String -Width 240).TrimEnd())
}
# Winlogon 7001/7002 tracks user session logon/logoff more reliably than Security 4624/4634.
$WinlogonFilter = @{
LogName = 'System'
ProviderName = 'Microsoft-Windows-Winlogon'
Id = @(7001, 7002)
}
$SecurityFilter = @{
2026-07-07 14:45:18 -04:00
LogName = 'Security'
2026-07-07 15:39:57 -04:00
Id = @(4800, 4801)
2026-07-07 14:45:18 -04:00
}
2026-07-07 17:30:11 -04:00
# Security 4624 type 7 is unlock authentication evidence. Query it separately so it cannot
# crowd out 4800/4801 workstation lock/unlock events in latest-event mode.
$UnlockAuthFilter = @{
LogName = 'Security'
Id = 4624
}
2026-07-07 15:39:57 -04:00
$PowerFilters = @(
@{
LogName = 'System'
ProviderName = 'Microsoft-Windows-Kernel-Power'
Id = 42
},
@{
LogName = 'System'
ProviderName = 'Microsoft-Windows-Power-Troubleshooter'
Id = 1
},
@{
LogName = 'System'
ProviderName = 'EventLog'
Id = @(6005, 6006)
},
@{
LogName = 'System'
ProviderName = 'User32'
Id = 1074
}
)
If ($UseDateRange) {
Try {
$StartDateTime = ConvertTo-ActivityDateTime -Value $Start -ParameterName 'Start'
$EndDateTime = ConvertTo-ActivityDateTime -Value $End -ParameterName 'End'
2026-07-07 14:45:18 -04:00
}
Catch {
Write-Error $_.Exception.Message
2026-07-07 14:45:18 -04:00
Exit 1
}
If ($EndDateTime -le $StartDateTime) {
Write-Error "-End must be later than -Start."
Exit 1
}
2026-07-07 15:23:15 -04:00
$WinlogonFilter.StartTime = $StartDateTime.LocalDateTime
$WinlogonFilter.EndTime = $EndDateTime.LocalDateTime
$SecurityFilter.StartTime = $StartDateTime.LocalDateTime
$SecurityFilter.EndTime = $EndDateTime.LocalDateTime
2026-07-07 17:30:11 -04:00
$UnlockAuthFilter.StartTime = $StartDateTime.LocalDateTime
$UnlockAuthFilter.EndTime = $EndDateTime.LocalDateTime
2026-07-07 15:39:57 -04:00
ForEach ($PowerFilter in $PowerFilters) {
$PowerFilter.StartTime = $StartDateTime.LocalDateTime
$PowerFilter.EndTime = $EndDateTime.LocalDateTime
}
2026-07-07 15:23:15 -04:00
$ActivityEvents = New-Object System.Collections.Generic.List[object]
ForEach ($Event in (Get-UserLogonActivityWinEvent -FilterHashtable $WinlogonFilter -LogDescription 'System Winlogon event log')) {
Add-UserLogonActivityRecord -ActivityEvents $ActivityEvents -ActivityEvent (ConvertTo-WinlogonUserActivity -Event $Event)
}
2026-07-07 15:23:15 -04:00
ForEach ($Event in (Get-UserLogonActivityWinEvent -FilterHashtable $SecurityFilter -LogDescription 'Security event log')) {
Add-UserLogonActivityRecord -ActivityEvents $ActivityEvents -ActivityEvent (ConvertTo-SecurityUserActivity -Event $Event)
}
2026-07-07 15:23:15 -04:00
2026-07-07 17:30:11 -04:00
ForEach ($Event in (Get-UserLogonActivityWinEvent -FilterHashtable $UnlockAuthFilter -LogDescription 'Security unlock-authentication event log')) {
Add-UserLogonActivityRecord -ActivityEvents $ActivityEvents -ActivityEvent (ConvertTo-SecurityUserActivity -Event $Event)
}
2026-07-07 15:39:57 -04:00
ForEach ($PowerFilter in $PowerFilters) {
ForEach ($Event in (Get-UserLogonActivityWinEvent -FilterHashtable $PowerFilter -LogDescription 'System power event log')) {
Add-UserLogonActivityRecord -ActivityEvents $ActivityEvents -ActivityEvent (ConvertTo-PowerUserActivity -Event $Event)
}
}
2026-07-07 15:23:15 -04:00
$OutputEvents = @(Get-UniqueUserLogonActivity -ActivityEvents $ActivityEvents | Sort-Object -Property TimeCreated)
Write-UserLogonActivityOutput -ActivityEvents $OutputEvents
2026-07-07 14:45:18 -04:00
}
Else {
$ActivityLimit = 15
$SearchLimit = 200
$MaxSearchLimit = 10000
2026-07-07 15:23:15 -04:00
$OutputEvents = @()
Do {
2026-07-07 15:23:15 -04:00
$ActivityEvents = New-Object System.Collections.Generic.List[object]
$WinlogonEvents = @(Get-UserLogonActivityWinEvent -FilterHashtable $WinlogonFilter -MaxEvents $SearchLimit -LogDescription 'System Winlogon event log')
$SecurityEvents = @(Get-UserLogonActivityWinEvent -FilterHashtable $SecurityFilter -MaxEvents $SearchLimit -LogDescription 'Security event log')
2026-07-07 17:30:11 -04:00
$UnlockAuthEvents = @(Get-UserLogonActivityWinEvent -FilterHashtable $UnlockAuthFilter -MaxEvents $SearchLimit -LogDescription 'Security unlock-authentication event log')
2026-07-07 15:39:57 -04:00
$PowerEvents = @()
$PowerFiltersMayHaveMoreEvents = $false
2026-07-07 15:23:15 -04:00
ForEach ($Event in $WinlogonEvents) {
Add-UserLogonActivityRecord -ActivityEvents $ActivityEvents -ActivityEvent (ConvertTo-WinlogonUserActivity -Event $Event)
}
2026-07-07 15:23:15 -04:00
ForEach ($Event in $SecurityEvents) {
Add-UserLogonActivityRecord -ActivityEvents $ActivityEvents -ActivityEvent (ConvertTo-SecurityUserActivity -Event $Event)
}
2026-07-07 17:30:11 -04:00
ForEach ($Event in $UnlockAuthEvents) {
Add-UserLogonActivityRecord -ActivityEvents $ActivityEvents -ActivityEvent (ConvertTo-SecurityUserActivity -Event $Event)
}
2026-07-07 15:39:57 -04:00
ForEach ($PowerFilter in $PowerFilters) {
$CurrentPowerEvents = @(Get-UserLogonActivityWinEvent -FilterHashtable $PowerFilter -MaxEvents $SearchLimit -LogDescription 'System power event log')
$PowerEvents += $CurrentPowerEvents
If ($CurrentPowerEvents.Count -ge $SearchLimit) {
$PowerFiltersMayHaveMoreEvents = $true
}
}
ForEach ($Event in $PowerEvents) {
Add-UserLogonActivityRecord -ActivityEvents $ActivityEvents -ActivityEvent (ConvertTo-PowerUserActivity -Event $Event)
}
2026-07-07 15:23:15 -04:00
$OutputEvents = @(Get-UniqueUserLogonActivity -ActivityEvents $ActivityEvents | Sort-Object -Property TimeCreated -Descending | Select-Object -First $ActivityLimit)
2026-07-07 17:30:11 -04:00
If ($OutputEvents.Count -ge $ActivityLimit -or (($WinlogonEvents.Count -lt $SearchLimit) -and ($SecurityEvents.Count -lt $SearchLimit) -and ($UnlockAuthEvents.Count -lt $SearchLimit) -and -not $PowerFiltersMayHaveMoreEvents) -or $SearchLimit -ge $MaxSearchLimit) {
Break
}
$SearchLimit = [Math]::Min(($SearchLimit * 2), $MaxSearchLimit)
} While ($true)
2026-07-07 14:45:18 -04:00
2026-07-07 15:23:15 -04:00
Write-UserLogonActivityOutput -ActivityEvents $OutputEvents
2026-07-07 14:45:18 -04:00
}