chore(home): mirror from kalinamall (038363c) with papatramp URLs

This commit is contained in:
2026-07-14 20:44:01 +10:00
commit 6d102603c2
46 changed files with 12612 additions and 0 deletions
+46
View File
@@ -0,0 +1,46 @@
<#
.SYNOPSIS
Smoke/autotests for RDP-login-monitor deploy and SAC paths.
.EXAMPLE
powershell.exe -NoProfile -ExecutionPolicy Bypass -File tools\Run-RdpMonitorTests.ps1
#>
[CmdletBinding()]
param()
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$testsDir = Join-Path $PSScriptRoot 'tests'
$suites = @(
'Test-ScriptSyntaxAll.ps1',
'Test-TaskQueryModule.ps1',
'Test-DeployTaskLimit.ps1',
'Test-SecurityPollCursor.ps1',
'Test-SendDeploySacNotice.ps1'
)
Write-Host '=== RDP-login-monitor autotests ==='
$failed = 0
foreach ($suite in $suites) {
$path = Join-Path $testsDir $suite
if (-not (Test-Path -LiteralPath $path)) {
Write-Host "FAIL: missing suite $path"
$failed++
continue
}
Write-Host "--- $suite ---"
try {
& $path
} catch {
Write-Host "SUITE FAILED: $suite - $($_.Exception.Message)"
$failed++
}
}
if ($failed -gt 0) {
Write-Host ('=== FAILED ({0} suite(s)) ===' -f $failed)
exit 1
}
Write-Host '=== ALL PASSED ==='
exit 0
+51
View File
@@ -0,0 +1,51 @@
<#
.SYNOPSIS
Просмотр недавних 4624 с полями для диагностики RDP-login-monitor.
.EXAMPLE
.\Show-Rdp4624Recent.ps1
.\Show-Rdp4624Recent.ps1 -Minutes 30 -User jdoe
#>
[CmdletBinding()]
param(
[int]$Minutes = 15,
[string]$User = '',
[int]$MaxEvents = 50
)
$start = (Get-Date).AddMinutes(-$Minutes)
Write-Host "Security 4624 since $($start.ToString('yyyy-MM-dd HH:mm:ss')) (local time)" -ForegroundColor Cyan
$events = @(Get-WinEvent -FilterHashtable @{
LogName = 'Security'
Id = 4624
StartTime = $start
} -MaxEvents $MaxEvents -ErrorAction SilentlyContinue)
if ($events.Count -eq 0) {
Write-Host 'No 4624 events in window.'
exit 0
}
function Get-EvProp($Event, [string]$Name) {
$xml = [xml]$Event.ToXml()
$n = $xml.Event.EventData.Data | Where-Object { $_.Name -eq $Name } | Select-Object -First 1
if ($null -eq $n) { return '-' }
return [string]$n.'#text'
}
$rows = foreach ($ev in $events) {
$u = Get-EvProp $ev 'TargetUserName'
if ($User -and $u -notlike "*$User*") { continue }
[pscustomobject]@{
TimeCreated = $ev.TimeCreated.ToString('yyyy-MM-dd HH:mm:ss')
RecordId = $ev.RecordId
User = $u
LogonType = Get-EvProp $ev 'LogonType'
IpAddress = Get-EvProp $ev 'IpAddress'
Workstation = Get-EvProp $ev 'WorkstationName'
Process = Get-EvProp $ev 'LogonProcessName'
}
}
$rows | Format-Table -AutoSize
Write-Host "`nTip: monitor log — Select-String -Path 'C:\ProgramData\RDP-login-monitor\Logs\*.log' -Pattern 'Notify:|Skip 4624|Notify dedup'"
+8
View File
@@ -0,0 +1,8 @@
$errs = $null
[void][System.Management.Automation.Language.Parser]::ParseFile(
(Join-Path $PSScriptRoot '..\Deploy-LoginMonitor.ps1'),
[ref]$null,
[ref]$errs
)
if ($errs) { $errs | ForEach-Object { $_.ToString() }; exit 1 }
Write-Output 'OK'
+55
View File
@@ -0,0 +1,55 @@
# Проверка парсинга UserData/EventInfo для RD Gateway 303 (BytesReceived != ErrorCode).
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$sample303 = @'
<Event xmlns="http://schemas.microsoft.com/win/2004/08/events/event">
<System>
<Provider Name="Microsoft-Windows-TerminalServices-Gateway" />
<EventID>303</EventID>
<TimeCreated SystemTime="2026-06-02T23:51:21.033855700Z" />
</System>
<UserData>
<EventInfo xmlns="aag">
<Username>CONTOSO\TSA</Username>
<IpAddress>95.154.72.73</IpAddress>
<Resource>192.168.164.43</Resource>
<BytesReceived>1991</BytesReceived>
<BytesTransfered>2116</BytesTransfered>
<SessionDuration>0</SessionDuration>
<ConnectionProtocol>HTTP</ConnectionProtocol>
<ErrorCode>1226</ErrorCode>
</EventInfo>
</UserData>
</Event>
'@
function Get-RDGatewayUserDataEventInfoMapFromXmlText {
param([string]$XmlText)
$map = @{}
$xml = [xml]$XmlText
$eventInfo = $xml.Event.UserData.EventInfo
foreach ($node in @($eventInfo.ChildNodes)) {
if ($null -eq $node -or $node.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue }
$map[$node.LocalName] = [string]$node.InnerText
}
return $map
}
$map = Get-RDGatewayUserDataEventInfoMapFromXmlText -XmlText $sample303
if ($map['ErrorCode'] -ne '1226') {
throw "Expected ErrorCode=1226, got $($map['ErrorCode'])"
}
if ($map['BytesReceived'] -ne '1991') {
throw "Expected BytesReceived=1991, got $($map['BytesReceived'])"
}
if ($map['SessionDuration'] -ne '0') {
throw "Expected SessionDuration=0, got $($map['SessionDuration'])"
}
Write-Host 'OK: RD Gateway EventInfo XML fields parsed correctly (ErrorCode != BytesReceived).'
# 1226 в sample — типичный штатный код закрытия туннеля (в Login_Monitor.ps1 → disconnected, не failed).
if ($map['ErrorCode'] -ne '1226') {
throw 'Expected sample ErrorCode 1226 for standard RDG disconnect'
}
Write-Host 'OK: sample 303 ErrorCode 1226 (standard RD Gateway disconnect).'
+8
View File
@@ -0,0 +1,8 @@
param([string]$Path = (Join-Path $PSScriptRoot '..\Login_Monitor.ps1'))
$errs = $null
[void][System.Management.Automation.Language.Parser]::ParseFile((Resolve-Path $Path), [ref]$null, [ref]$errs)
if ($errs) {
$errs | ForEach-Object { $_.ToString() }
exit 1
}
Write-Output 'OK'
+33
View File
@@ -0,0 +1,33 @@
. (Join-Path $PSScriptRoot '_TestLib.ps1')
. (Join-Path $PSScriptRoot '_DeployFunctionsLoader.ps1')
$repo = Get-RdpMonitorRepoRoot
Invoke-RdpMonitorTestCase -Name 'Deploy functions load (RDP_DEPLOY_FUNCTIONS_ONLY)' -Script {
Assert-CommandExists -Name 'Initialize-RdpMonitorDeployTaskQuery'
Assert-CommandExists -Name 'Test-RdpMonitorDeployMainTaskNeedsUnlimitedExecutionTime'
Assert-CommandExists -Name 'Write-RdpMonitorDeployScheduledTaskVerification'
Assert-CommandExists -Name 'Get-RdpMonitorDeployTaskExecutionTimeLimitLabelFromResolved'
Assert-CommandExists -Name 'Convert-RdpMonitorDeployTaskExecutionTimeLimitValue'
}
Invoke-RdpMonitorTestCase -Name 'Deploy ExecutionTimeLimit accepts PT0S string (Get-ScheduledTask shape)' -Script {
Assert-True -Condition (Test-RdpMonitorDeployTaskExecutionLimitUnlimitedValue -Limit 'PT0S') `
-Message 'PT0S string must be treated as unlimited'
$resolved = [pscustomobject]@{ Limit = 'PT0S'; Source = 'Get-ScheduledTask' }
$label = Get-RdpMonitorDeployTaskExecutionTimeLimitLabelFromResolved -Resolved $resolved
Assert-True -Condition ($label -eq 'PT0S') -Message "Expected PT0S label, got $label"
Assert-True -Condition (Test-RdpMonitorDeployTaskExecutionLimitUnlimitedValue -Limit $resolved.Limit) `
-Message 'Resolved PT0S string must pass unlimited check'
}
Invoke-RdpMonitorTestCase -Name 'Deploy pre-check task limit (no throw on early path)' -Script {
$needsFix = Test-RdpMonitorDeployMainTaskNeedsUnlimitedExecutionTime -ShareRoot $repo -TaskName 'RDP-Login-Monitor-UnitTest-Missing'
Assert-True -Condition ($needsFix -is [bool]) -Message 'Test-RdpMonitorDeployMainTaskNeedsUnlimitedExecutionTime must return bool'
}
Invoke-RdpMonitorTestCase -Name 'Deploy verification after Initialize (no missing command)' -Script {
[void](Initialize-RdpMonitorDeployTaskQuery -ShareRoot $repo)
Assert-CommandExists -Name 'Get-RdpMonitorScheduledTaskExecutionTimeLimitResolved'
$ok = Write-RdpMonitorDeployScheduledTaskVerification -ShareRoot $repo -TaskName 'RDP-Login-Monitor-UnitTest-Missing'
Assert-True -Condition ($ok -is [bool]) -Message 'Write-RdpMonitorDeployScheduledTaskVerification must return bool'
}
+22
View File
@@ -0,0 +1,22 @@
. (Join-Path $PSScriptRoot '_TestLib.ps1')
$repo = Get-RdpMonitorRepoRoot
$files = @(
'Deploy-LoginMonitor.ps1',
'Login_Monitor.ps1',
'Sac-Client.ps1',
'RdpMonitor-TaskQuery.ps1',
'update-rdp-monitor.ps1'
)
foreach ($rel in $files) {
$path = Join-Path $repo $rel
Invoke-RdpMonitorTestCase -Name "Syntax: $rel" -Script {
Assert-True -Condition (Test-Path -LiteralPath $path) -Message "Missing $path"
$errs = $null
[void][System.Management.Automation.Language.Parser]::ParseFile($path, [ref]$null, [ref]$errs)
if ($errs -and $errs.Count -gt 0) {
throw ($errs | ForEach-Object { $_.ToString() } | Out-String)
}
}
}
+49
View File
@@ -0,0 +1,49 @@
. (Join-Path $PSScriptRoot '_TestLib.ps1')
function Test-RdpSecurityPollCursorResolve {
param(
[datetime]$Now,
[int]$MaxAgeMinutes,
[Nullable[datetime]]$SavedCursor
)
$maxAgeMin = [math]::Max(1, $MaxAgeMinutes)
$lookbackFloor = $Now.AddMinutes(-1 * $maxAgeMin)
if ($null -eq $SavedCursor) {
return $lookbackFloor
}
if ($SavedCursor -lt $lookbackFloor) {
return $lookbackFloor
}
return $SavedCursor
}
Invoke-RdpMonitorTestCase -Name 'Security cursor: missing file uses lookback floor' -Script {
$now = Get-Date '2026-06-15T12:00:00'
$resolved = Test-RdpSecurityPollCursorResolve -Now $now -MaxAgeMinutes 60 -SavedCursor $null
$expected = $now.AddMinutes(-60)
Assert-True -Condition ($resolved -eq $expected) -Message 'Expected lookback floor when cursor missing'
}
Invoke-RdpMonitorTestCase -Name 'Security cursor: stale saved cursor capped to lookback floor' -Script {
$now = Get-Date '2026-06-15T12:00:00'
$stale = $now.AddMinutes(-120)
$resolved = Test-RdpSecurityPollCursorResolve -Now $now -MaxAgeMinutes 60 -SavedCursor $stale
$expected = $now.AddMinutes(-60)
Assert-True -Condition ($resolved -eq $expected) -Message 'Expected cap at lookback floor for stale cursor'
}
Invoke-RdpMonitorTestCase -Name 'Security cursor: recent saved cursor preserved' -Script {
$now = Get-Date '2026-06-15T12:00:00'
$recent = $now.AddMinutes(-5)
$resolved = Test-RdpSecurityPollCursorResolve -Now $now -MaxAgeMinutes 60 -SavedCursor $recent
Assert-True -Condition ($resolved -eq $recent) -Message 'Expected recent cursor unchanged'
}
Invoke-RdpMonitorTestCase -Name 'Login_Monitor defines Security poll cursor helpers' -Script {
$repo = Get-RdpMonitorRepoRoot
$text = Get-Content -LiteralPath (Join-Path $repo 'Login_Monitor.ps1') -Raw
Assert-True -Condition ($text -match 'Get-RdpSecurityPollCursor') -Message 'Missing Get-RdpSecurityPollCursor'
Assert-True -Condition ($text -match 'Set-RdpSecurityPollCursor') -Message 'Missing Set-RdpSecurityPollCursor'
Assert-True -Condition ($text -match '\$SecurityPollCursorFile') -Message 'Missing SecurityPollCursorFile'
}
+75
View File
@@ -0,0 +1,75 @@
. (Join-Path $PSScriptRoot '_TestLib.ps1')
function Test-RdpMonitorDeploySacHostLabelStrictMode {
Set-StrictMode -Version Latest
$hostLabel = [string]$env:COMPUTERNAME
if (Get-Variable -Name ServerDisplayName -Scope Script -ErrorAction SilentlyContinue) {
$sdn = (Get-Variable -Name ServerDisplayName -Scope Script -ValueOnly)
if ($null -ne $sdn -and -not [string]::IsNullOrWhiteSpace([string]$sdn)) {
$hostLabel = [string]$sdn.Trim()
}
}
return $hostLabel
}
Invoke-RdpMonitorTestCase -Name 'Deploy SAC host label without ServerDisplayName (StrictMode)' -Script {
$label = Test-RdpMonitorDeploySacHostLabelStrictMode
Assert-True -Condition (-not [string]::IsNullOrWhiteSpace($label)) -Message 'Host label must not be empty'
Assert-True -Condition ($label -eq [string]$env:COMPUTERNAME) -Message 'Expected COMPUTERNAME when ServerDisplayName unset'
}
Invoke-RdpMonitorTestCase -Name 'Deploy SAC host label with ServerDisplayName (StrictMode)' -Script {
$script:ServerDisplayName = 'Test-Server-Display'
try {
$label = Test-RdpMonitorDeploySacHostLabelStrictMode
Assert-True -Condition ($label -eq 'Test-Server-Display') -Message 'Expected ServerDisplayName value'
} finally {
Remove-Variable -Name ServerDisplayName -Scope Script -ErrorAction SilentlyContinue
}
}
Invoke-RdpMonitorTestCase -Name 'Login_Monitor -SendDeploySacNotice does not fail on StrictMode host label' -Script {
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = New-Object Security.Principal.WindowsPrincipal($identity)
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
Write-Host 'SKIP: requires elevated PowerShell (administrator)'
return
}
$repo = Get-RdpMonitorRepoRoot
$tempRoot = Join-Path $env:TEMP ("rdp-monitor-test-{0}" -f [guid]::NewGuid().ToString('N'))
New-Item -ItemType Directory -Path $tempRoot -Force | Out-Null
New-Item -ItemType Directory -Path (Join-Path $tempRoot 'Logs') -Force | Out-Null
$settings = @'
$UseSAC = 'off'
'@
$settingsPath = Join-Path $tempRoot 'login_monitor.settings.ps1'
[System.IO.File]::WriteAllText($settingsPath, $settings, (New-Object System.Text.UTF8Encoding $true))
Copy-Item -LiteralPath (Join-Path $repo 'Login_Monitor.ps1') -Destination (Join-Path $tempRoot 'Login_Monitor.ps1') -Force
Copy-Item -LiteralPath (Join-Path $repo 'Sac-Client.ps1') -Destination (Join-Path $tempRoot 'Sac-Client.ps1') -Force
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe"
$psi.Arguments = '-NoProfile -ExecutionPolicy Bypass -File "{0}" -SendDeploySacNotice' -f (Join-Path $tempRoot 'Login_Monitor.ps1')
$psi.WorkingDirectory = $tempRoot
$psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true
$psi.UseShellExecute = $false
$psi.CreateNoWindow = $true
$proc = [System.Diagnostics.Process]::Start($psi)
$stdout = $proc.StandardOutput.ReadToEnd()
$stderr = $proc.StandardError.ReadToEnd()
$proc.WaitForExit()
try {
if ($stderr -match 'ServerDisplayName|VariableIsUndefined') {
throw "SendDeploySacNotice stderr contains StrictMode error: $stderr"
}
Assert-True -Condition ($proc.ExitCode -eq 0) -Message "Expected exit 0 with UseSAC=off, got $($proc.ExitCode); stderr=$stderr stdout=$stdout"
} finally {
Remove-Item -LiteralPath $tempRoot -Recurse -Force -ErrorAction SilentlyContinue
}
}
+15
View File
@@ -0,0 +1,15 @@
. (Join-Path $PSScriptRoot '_TestLib.ps1')
$repo = Get-RdpMonitorRepoRoot
$taskQuery = Join-Path $repo 'RdpMonitor-TaskQuery.ps1'
Invoke-RdpMonitorTestCase -Name 'TaskQuery file exists' -Script {
Assert-True -Condition (Test-Path -LiteralPath $taskQuery) -Message "Missing $taskQuery"
}
Invoke-RdpMonitorTestCase -Name 'TaskQuery dot-source defines core commands' -Script {
. $taskQuery
Assert-CommandExists -Name 'Get-RdpMonitorScheduledTaskExecutionTimeLimitResolved'
Assert-CommandExists -Name 'Test-RdpMonitorScheduledTaskNeedsUnlimitedExecutionTimeLimit'
Assert-CommandExists -Name 'Get-RdpMonitorScheduledTaskExecutionTimeLimitLabel'
}
+16
View File
@@ -0,0 +1,16 @@
# Dot-source from a test .ps1 at script scope after _TestLib.ps1 (not from a function/scriptblock).
if ($script:RdpMonitorDeployFunctionsLoaded) { return }
$repo = Get-RdpMonitorRepoRoot
$prev = $env:RDP_DEPLOY_FUNCTIONS_ONLY
$env:RDP_DEPLOY_FUNCTIONS_ONLY = '1'
try {
. (Join-Path $repo 'Deploy-LoginMonitor.ps1')
} finally {
if ($null -eq $prev) {
Remove-Item Env:RDP_DEPLOY_FUNCTIONS_ONLY -ErrorAction SilentlyContinue
} else {
$env:RDP_DEPLOY_FUNCTIONS_ONLY = $prev
}
}
$script:RdpMonitorDeployFunctionsLoaded = $true
+39
View File
@@ -0,0 +1,39 @@
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$script:RdpMonitorDeployFunctionsLoaded = $false
function Get-RdpMonitorRepoRoot { return (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
}
function Assert-True {
param(
[Parameter(Mandatory = $true)][bool]$Condition,
[Parameter(Mandatory = $true)][string]$Message
)
if (-not $Condition) {
throw "FAIL: $Message"
}
}
function Assert-CommandExists {
param(
[Parameter(Mandatory = $true)][string]$Name
)
$cmd = Get-Command -Name $Name -ErrorAction SilentlyContinue
Assert-True -Condition ($null -ne $cmd) -Message "Command not found: $Name"
}
function Invoke-RdpMonitorTestCase {
param(
[Parameter(Mandatory = $true)][string]$Name,
[Parameter(Mandatory = $true)][scriptblock]$Script
)
try {
& $Script
Write-Host "PASS: $Name"
} catch {
Write-Host "FAIL: $Name - $($_.Exception.Message)"
throw
}
}