|
| 1 | +# Verifies that live Security event log collection works when elevated. |
| 2 | +# |
| 3 | +# Windows restricts the Security channel to elevated processes, and being |
| 4 | +# signed in as an Administrator is not enough: under UAC an ordinary window |
| 5 | +# gets a filtered token. This script can only be run from an elevated window, |
| 6 | +# so it covers the one path the normal test suite cannot: that an elevated |
| 7 | +# collection actually returns events. |
| 8 | +# |
| 9 | +# Usage: open PowerShell with Run as Administrator, then |
| 10 | +# .\verify-elevated.ps1 |
| 11 | + |
| 12 | +$ErrorActionPreference = "Stop" |
| 13 | + |
| 14 | +$identity = [Security.Principal.WindowsIdentity]::GetCurrent() |
| 15 | +$principal = New-Object Security.Principal.WindowsPrincipal($identity) |
| 16 | +$elevated = $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) |
| 17 | + |
| 18 | +Write-Host "User : $($identity.Name)" |
| 19 | +Write-Host "Elevated : $elevated" |
| 20 | + |
| 21 | +if (-not $elevated) { |
| 22 | + Write-Host "" |
| 23 | + Write-Host "This window is not elevated, so the check cannot run." -ForegroundColor Yellow |
| 24 | + Write-Host "Being an Administrator is not enough; the window itself must be elevated." |
| 25 | + Write-Host "Right-click PowerShell, choose Run as Administrator, then run this again." |
| 26 | + exit 1 |
| 27 | +} |
| 28 | + |
| 29 | +$candidates = @( |
| 30 | + (Join-Path $env:LOCALAPPDATA "Programs\Python\Python313\python.exe"), |
| 31 | + (Join-Path $env:LOCALAPPDATA "Programs\Python\Python312\python.exe"), |
| 32 | + (Join-Path $env:LOCALAPPDATA "Programs\Python\Python311\python.exe"), |
| 33 | + (Join-Path $env:LOCALAPPDATA "Programs\Python\Python310\python.exe"), |
| 34 | + "py.exe", |
| 35 | + "python.exe" |
| 36 | +) |
| 37 | +$python = $null |
| 38 | +foreach ($candidate in $candidates) { |
| 39 | + try { |
| 40 | + if ([System.IO.Path]::IsPathRooted($candidate) -and -not (Test-Path -LiteralPath $candidate)) { continue } |
| 41 | + $version = & $candidate --version 2>&1 |
| 42 | + if ($LASTEXITCODE -eq 0 -and "$version" -match "^Python 3\.(1[0-9]|[2-9][0-9])") { $python = $candidate; break } |
| 43 | + } catch { continue } |
| 44 | +} |
| 45 | +if (-not $python) { Write-Error "Python 3.10 or newer is required." } |
| 46 | + |
| 47 | +# Resolve up front: Start-Process reports a missing WorkingDirectory with a |
| 48 | +# message that does not name the path, which is hard to act on. |
| 49 | +$v2Dir = Join-Path $PSScriptRoot "v2.0" |
| 50 | +$v1Dir = Join-Path $PSScriptRoot "v1.0" |
| 51 | +foreach ($dir in @($v2Dir, $v1Dir)) { |
| 52 | + if (-not (Test-Path -LiteralPath $dir -PathType Container)) { |
| 53 | + Write-Error "Application folder not found: $dir. Run this script from the repository root." |
| 54 | + } |
| 55 | +} |
| 56 | + |
| 57 | +$results = [System.Collections.Generic.List[object]]::new() |
| 58 | +function Add-Result([string]$Name, [bool]$Ok, [string]$Detail) { |
| 59 | + $results.Add([pscustomobject]@{ Name = $Name; Ok = $Ok; Detail = $Detail }) |
| 60 | + $status = if ($Ok) { "PASS" } else { "FAIL" } |
| 61 | + $colour = if ($Ok) { "Green" } else { "Red" } |
| 62 | + Write-Host (" {0} {1} {2}" -f $status, $Name.PadRight(46), $Detail) -ForegroundColor $colour |
| 63 | +} |
| 64 | + |
| 65 | +function Stop-ServiceProcess($Process, [string]$DatabasePath) { |
| 66 | + # Wait for the process to actually exit before deleting its database. |
| 67 | + # Stop-Process returns before the SQLite handle is released, so removing |
| 68 | + # the file immediately fails and leaves it behind in the temp folder. |
| 69 | + if ($Process) { |
| 70 | + Stop-Process -Id $Process.Id -Force -ErrorAction SilentlyContinue |
| 71 | + try { $Process.WaitForExit(5000) | Out-Null } catch { } |
| 72 | + } |
| 73 | + if ($DatabasePath) { |
| 74 | + foreach ($attempt in 1..5) { |
| 75 | + Remove-Item -LiteralPath $DatabasePath -Force -ErrorAction SilentlyContinue |
| 76 | + if (-not (Test-Path -LiteralPath $DatabasePath)) { break } |
| 77 | + Start-Sleep -Milliseconds 300 |
| 78 | + } |
| 79 | + } |
| 80 | +} |
| 81 | + |
| 82 | +function Wait-ForService([string]$Url, [int]$TimeoutSeconds = 30) { |
| 83 | + $deadline = (Get-Date).AddSeconds($TimeoutSeconds) |
| 84 | + while ((Get-Date) -lt $deadline) { |
| 85 | + try { |
| 86 | + Invoke-RestMethod $Url -TimeoutSec 3 | Out-Null |
| 87 | + return $true |
| 88 | + } catch { |
| 89 | + Start-Sleep -Milliseconds 400 |
| 90 | + } |
| 91 | + } |
| 92 | + return $false |
| 93 | +} |
| 94 | + |
| 95 | +Write-Host "" |
| 96 | +Write-Host "1. Direct Get-WinEvent on the Security channel" |
| 97 | +try { |
| 98 | + $events = Get-WinEvent -LogName 'Security' -MaxEvents 3 -ErrorAction Stop |
| 99 | + Add-Result "Security log readable directly" ($events.Count -gt 0) "$($events.Count) events" |
| 100 | +} catch { |
| 101 | + Add-Result "Security log readable directly" $false $_.Exception.Message |
| 102 | +} |
| 103 | + |
| 104 | +# --- v2.0 ------------------------------------------------------------------- |
| 105 | +Write-Host "" |
| 106 | +Write-Host "2. SentinelOps v2.0 API" |
| 107 | +$v2Port = 8097 |
| 108 | +$v2Db = Join-Path $env:TEMP "sentinelops-verify-v2.db" |
| 109 | +$v2Proc = $null |
| 110 | +try { |
| 111 | + $env:SENTINELOPS_V2_DB = $v2Db |
| 112 | + $env:SENTINELOPS_V2_PORT = "$v2Port" |
| 113 | + $v2Proc = Start-Process -FilePath $python -ArgumentList "-m", "backend.app" ` |
| 114 | + -WorkingDirectory $v2Dir -PassThru -WindowStyle Hidden |
| 115 | + |
| 116 | + if (-not (Wait-ForService "http://127.0.0.1:$v2Port/api/v2/status")) { |
| 117 | + Add-Result "v2.0 service started" $false "did not respond in time" |
| 118 | + } else { |
| 119 | + $status = Invoke-RestMethod "http://127.0.0.1:$v2Port/api/v2/status" |
| 120 | + Add-Result "v2.0 status reports elevated" ($status.elevated -eq $true) "elevated=$($status.elevated)" |
| 121 | + |
| 122 | + $restricted = @($status.restrictedChannels).Count |
| 123 | + Add-Result "v2.0 lists no restricted channels" ($restricted -eq 0) "$restricted entries" |
| 124 | + |
| 125 | + $collected = Invoke-RestMethod "http://127.0.0.1:$v2Port/api/v2/events/windows?channel=Security&max=5" |
| 126 | + Add-Result "v2.0 Security collection returns events" ($collected.newCount -gt 0) "$($collected.newCount) events" |
| 127 | + } |
| 128 | +} catch { |
| 129 | + Add-Result "v2.0 Security collection returns events" $false $_.Exception.Message |
| 130 | +} finally { |
| 131 | + Stop-ServiceProcess $v2Proc $v2Db |
| 132 | + Remove-Item Env:\SENTINELOPS_V2_DB -ErrorAction SilentlyContinue |
| 133 | + Remove-Item Env:\SENTINELOPS_V2_PORT -ErrorAction SilentlyContinue |
| 134 | +} |
| 135 | + |
| 136 | +# --- v1.0 ------------------------------------------------------------------- |
| 137 | +Write-Host "" |
| 138 | +Write-Host "3. SentinelOps v1.0 API" |
| 139 | +$v1Port = 8096 |
| 140 | +$v1Db = Join-Path $env:TEMP "sentinelops-verify-v1.db" |
| 141 | +$v1Proc = $null |
| 142 | +try { |
| 143 | + $env:SENTINELOPS_PORT = "$v1Port" |
| 144 | + $env:SENTINELOPS_DB = $v1Db |
| 145 | + $v1Proc = Start-Process -FilePath $python -ArgumentList "server.py" ` |
| 146 | + -WorkingDirectory $v1Dir -PassThru -WindowStyle Hidden |
| 147 | + |
| 148 | + if (-not (Wait-ForService "http://127.0.0.1:$v1Port/api/status")) { |
| 149 | + Add-Result "v1.0 service started" $false "did not respond in time" |
| 150 | + } else { |
| 151 | + $status = Invoke-RestMethod "http://127.0.0.1:$v1Port/api/status" |
| 152 | + Add-Result "v1.0 status reports elevated" ($status.elevated -eq $true) "elevated=$($status.elevated)" |
| 153 | + |
| 154 | + $collected = Invoke-RestMethod "http://127.0.0.1:$v1Port/api/windows-events?channel=Security&max=5" |
| 155 | + Add-Result "v1.0 Security collection returns events" ($collected.newCount -gt 0) "$($collected.newCount) events" |
| 156 | + } |
| 157 | +} catch { |
| 158 | + Add-Result "v1.0 Security collection returns events" $false $_.Exception.Message |
| 159 | +} finally { |
| 160 | + Stop-ServiceProcess $v1Proc $v1Db |
| 161 | + Remove-Item Env:\SENTINELOPS_PORT -ErrorAction SilentlyContinue |
| 162 | + Remove-Item Env:\SENTINELOPS_DB -ErrorAction SilentlyContinue |
| 163 | +} |
| 164 | + |
| 165 | +Write-Host "" |
| 166 | +$failed = @($results | Where-Object { -not $_.Ok }) |
| 167 | +Write-Host ("{0}/{1} checks passed" -f ($results.Count - $failed.Count), $results.Count) |
| 168 | +if ($failed.Count -gt 0) { |
| 169 | + Write-Host "Elevated collection is NOT working. See the failures above." -ForegroundColor Red |
| 170 | + exit 1 |
| 171 | +} |
| 172 | +Write-Host "Elevated Security collection works end to end." -ForegroundColor Green |
| 173 | +exit 0 |
0 commit comments